...

Text file src/github.com/lestrrat-go/option/README.md

Documentation: github.com/lestrrat-go/option

     1# option
     2
     3Base object for the "Optional Parameters Pattern".
     4
     5# DESCRIPTION
     6
     7The beauty of this pattern is that you can achieve a method that can
     8take the following simple calling style
     9
    10```go
    11obj.Method(mandatory1, mandatory2)
    12```
    13
    14or the following, if you want to modify its behavior with optional parameters
    15
    16```go
    17obj.Method(mandatory1, mandatory2, optional1, optional2, optional3)
    18```
    19
    20Instead of the more clunky zero value for optionals style
    21
    22```go
    23obj.Method(mandatory1, mandatory2, nil, "", 0)
    24```
    25
    26or the equally clunky config object style, which requires you to create a
    27struct with `NamesThatLookReallyLongBecauseItNeedsToIncludeMethodNamesConfig	
    28
    29```go
    30cfg := &ConfigForMethod{
    31 Optional1: ...,
    32 Optional2: ...,
    33 Optional3: ...,
    34}
    35obj.Method(mandatory1, mandatory2, &cfg)
    36```
    37
    38# SYNOPSIS 
    39
    40Create an "identifier" for the option. We recommend using an unexported empty struct,
    41because
    42
    431. It is uniquely identifiable globally
    441. Takes minimal space
    451. Since it's unexported, you do not have to worry about it leaking elsewhere or having it changed by consumers
    46
    47```go
    48// an unexported empty struct
    49type identFeatureX struct{} 
    50```
    51
    52Then define a method to create an option using this identifier. Here we assume
    53that the option will be a boolean option.
    54
    55```go
    56// this is optional, but for readability we usually use a wrapper
    57// around option.Interface, or a type alias.
    58type Option
    59func WithFeatureX(v bool) Option {
    60  // use the constructor to create a new option
    61  return option.New(identFeatureX{}, v)
    62}
    63```
    64
    65Now you can create an option, which essentially a two element tuple consisting
    66of an identifier and its associated value.
    67
    68To consume this, you will need to create a function with variadic parameters,
    69and iterate over the list looking for a particular identifier:
    70
    71```go
    72func MyAwesomeFunc( /* mandatory parameters omitted */, options ...[]Option) {
    73  var enableFeatureX bool
    74  // The nolint directive is recommended if you are using linters such
    75  // as golangci-lint
    76  //nolint:forcetypeassert 
    77  for _, option := range options {
    78    switch option.Ident() {
    79    case identFeatureX{}:
    80      enableFeatureX = option.Value().(bool)
    81    // other cases omitted
    82    }
    83  }
    84  if enableFeatureX {
    85    ....
    86  }
    87}
    88```
    89
    90# Option objects
    91
    92Option objects take two arguments, its identifier and the value it contains.
    93
    94The identifier can be anything, but it's usually better to use a an unexported
    95empty struct so that only you have the ability to generate said option:
    96
    97```go
    98type identOptionalParamOne struct{}
    99type identOptionalParamTwo struct{}
   100type identOptionalParamThree struct{}
   101
   102func WithOptionOne(v ...) Option {
   103	return option.New(identOptionalParamOne{}, v)
   104}
   105```
   106
   107Then you can call the method we described above as
   108
   109```go
   110obj.Method(m1, m2, WithOptionOne(...), WithOptionTwo(...), WithOptionThree(...))
   111```
   112
   113Options should be parsed in a code that looks somewhat like this
   114
   115```go
   116func (obj *Object) Method(m1 Type1, m2 Type2, options ...Option) {
   117  paramOne := defaultValueParamOne
   118  for _, option := range options {
   119    switch option.Ident() {
   120    case identOptionalParamOne{}:
   121      paramOne = option.Value().(...)
   122    }
   123  }
   124  ...
   125}
   126```
   127
   128The loop requires a bit of boilerplate, and admittedly, this is the main downside
   129of this module. However, if you think you want use the Option as a Function pattern,
   130please check the FAQ below for rationale.
   131
   132# Simple usage
   133
   134Most of the times all you need to do is to declare the Option type as an alias
   135in your code:
   136
   137```go
   138package myawesomepkg
   139
   140import "github.com/lestrrat-go/option"
   141
   142type Option = option.Interface
   143```
   144
   145Then you can start defining options like they are described in the SYNOPSIS section.
   146
   147# Differentiating Options
   148
   149When you have multiple methods and options, and those options can only be passed to
   150each one the methods, it's hard to see which options should be passed to which method.
   151
   152```go
   153func WithX() Option { ... }
   154func WithY() Option { ... }
   155
   156// Now, which of WithX/WithY go to which method?
   157func (*Obj) Method1(options ...Option) {}
   158func (*Obj) Method2(options ...Option) {}
   159```
   160
   161In this case the easiest way to make it obvious is to put an extra layer around
   162the options so that they have different types
   163
   164```go
   165type Method1Option interface {
   166  Option
   167  method1Option()
   168}
   169
   170type method1Option struct { Option }
   171func (*method1Option) method1Option() {}
   172
   173func WithX() Method1Option {
   174  return &methodOption{option.New(...)}
   175}
   176
   177func (*Obj) Method1(options ...Method1Option) {}
   178```
   179
   180This way the compiler knows if an option can be passed to a given method.
   181
   182# FAQ
   183
   184## Why aren't these function-based?
   185
   186Using a base option type like `type Option func(ctx interface{})` is certainly one way to achieve the same goal. In this case, you are giving the option itself the ability to "configure" the main object. For example:
   187
   188```go
   189type Foo struct {
   190  optionaValue bool
   191}
   192
   193type Option func(*Foo) error
   194
   195func WithOptionalValue(v bool) Option {
   196  return Option(func(f *Foo) error {
   197    f.optionalValue = v
   198    return nil
   199  })
   200}
   201
   202func NewFoo(options ...Option) (*Foo, error) {
   203  var f Foo
   204  for _, o := range options {
   205    if err := o(&f); err != nil {
   206      return nil, err
   207    }
   208  }
   209  return &f
   210}
   211```
   212
   213This in itself is fine, but we think there are a few problems:
   214
   215### 1. It's hard to create a reusable "Option" type
   216
   217We create many libraries using this optional pattern. We would like to provide a default base object. However, this function based approach is not reusuable because each "Option" type requires that it has a context-specific input type. For example, if the "Option" type in the previous example was `func(interface{}) error`, then its usability will significantly decrease because of the type conversion.
   218
   219This is not to say that this library's approach is better as it also requires type conversion to convert the _value_ of the option. However, part of the beauty of the original function based approach was the ease of its use, and we claim that this significantly decreases the merits of the function based approach.
   220
   221### 2. The receiver requires exported fields
   222
   223Part of the appeal for a function-based option pattern is by giving the option itself the ability to do what it wants, you open up the possibility of allowing third-parties to create options that do things that the library authors did not think about.
   224
   225```go
   226package thirdparty
   227, but when I read drum sheet music, I kind of get thrown off b/c many times it says to hit the bass drum where I feel like it's a snare hit.
   228func WithMyAwesomeOption( ... ) mypkg.Option  {
   229  return mypkg.Option(func(f *mypkg) error {
   230    f.X = ...
   231    f.Y = ...
   232    f.Z = ...
   233    return nil
   234  })
   235}
   236```
   237
   238However, for any third party code to access and set field values, these fields (`X`, `Y`, `Z`) must be exported. Basically you will need an "open" struct.
   239
   240Exported fields are absolutely no problem when you have a struct that represents data alone (i.e., API calls that refer or change state information) happen, but we think that casually expose fields for a library struct is a sure way to maintenance hell in the future. What happens when you want to change the API? What happens when you realize that you want to use the field as state (i.e. use it for more than configuration)? What if they kept referring to that field, and then you have concurrent code accessing it?
   241
   242Giving third parties complete access to exported fields is like handing out a loaded weapon to the users, and you are at their mercy.
   243
   244Of course, providing public APIs for everything so you can validate and control concurrency is an option, but then ... it's a lot of work, and you may have to provide APIs _only_ so that users can refer it in the option-configuration phase. That sounds like a lot of extra work.
   245

View as plain text