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