Skip to content

Commit b34fb4d

Browse files
committed
human
1 parent 2ca19e5 commit b34fb4d

File tree

1 file changed

+115
-162
lines changed

1 file changed

+115
-162
lines changed

docs/rfcs/functional-composition-pattern.md

Lines changed: 115 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
## Overview
44

55
This codebase uses a **Functional Composition** pattern for its core
6-
interfaces. This pattern decomposes individual methods from interface
7-
types into corresponding function types, then recomposes them into a
8-
concrete implementation type. This approach provides flexibility,
9-
testability, and safe interface evolution for the OpenTelemetry
10-
Collector.
6+
interfaces. This pattern decomposes the individual methods from
7+
interface types into corresponding function types, then recomposes
8+
them into a concrete implementation type. This approach provides
9+
flexibility, testability, and safe interface evolution for the
10+
OpenTelemetry Collector.
1111

1212
When an interface type is exported for users outside of this
13-
repository, the type MUST follow these guidelines. Interface types
14-
exposed in internal packages may decide to export internal interfaces,
15-
of course.
13+
repository, the type MUST follow these guidelines, including the use
14+
of an unexported method to "seal" the interface. Interface types
15+
exposed from internal packages may opt-out of this recommendation.
1616

1717
For every method in the public interface, a corresponding `type
1818
<Method>Func func(...) ...` declaration in the same package will
@@ -33,12 +33,9 @@ These "sealed concrete" implementation objects support adding new
3333
methods in future releases, _without changing the major version
3434
number_, because public interface types are always provided through a
3535
package-provided implementation. As a key requirement, every function
36-
must have a simple "no-op" implementation correspodning with the zero
36+
must have a simple "no-op" implementation corresponding with the zero
3737
value of the `<Method>Func`.
3838

39-
Additional methods may be provided using the widely-known [Functional
40-
Option pattern](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html).
41-
4239
## Key concepts
4340

4441
### 1. Decompose Interfaces into Function Types
@@ -60,48 +57,85 @@ type CancelFunc func()
6057
// Function types implement their corresponding methods
6158
func (f WaitTimeFunc) WaitTime() time.Duration {
6259
if f == nil {
63-
return 0
60+
return 0 // No-op behavior
6461
}
6562
return f()
6663
}
6764

6865
func (f CancelFunc) Cancel() {
6966
if f == nil {
70-
return
67+
return // No-op behavior
7168
}
7269
f()
7370
}
7471
```
7572

76-
Users of the `net/http` package have seen this pattern before. The
77-
`http.HandlerFunc` type can be seen as the prototype for functional
78-
composition, in this case for HTTP handlers. Interestingly, the
79-
single-method `http.RoundTripper` interface, which represents the same
80-
interaction on the client-side, does not have a `RoundTripperFunc`. In
81-
this codebase, the pattern is applied exstensively.
73+
Users of the [`net/http` package have seen this
74+
pattern](https://pkg.go.dev/net/http#HandlerFunc). `http.HandlerFunc`
75+
can be seen as a prototype for the Functional Composition pattern, in
76+
this case for HTTP servers. Interestingly, the single-method
77+
`http.RoundTripper` interface, representing the same interaction for
78+
HTTP clients, does not have a `RoundTripperFunc`. In this codebase,
79+
the pattern is applied exstensively.
8280

83-
### 2. Compose Functions into Interface Implementations
81+
### 2. Compose Function Types into Interface Implementations
8482

85-
Create concrete implementations embed the corresponding function type:
83+
Create concrete implementations embedding the function type
84+
corresponding with each interface method:
8685

8786
```go
8887
type rateReservationImpl struct {
8988
WaitTimeFunc
9089
CancelFunc
9190
}
9291

93-
// Constructor for an instance
92+
This pattern applies even for single-method interfaces, where the
93+
`<Method>Func` is capable of implementing the interaface.
94+
95+
type RateLimiter interface {
96+
ReserveRate(context.Context, int) RateReservation
97+
}
98+
99+
type ReserveRateFunc func(context.Context, int) RateReservation
100+
101+
func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) RateReservation {
102+
if f == nil {
103+
return rateReservationImpl{} // Composite no-op behavior
104+
}
105+
f(ctx, value)
106+
}
107+
108+
// Implement the interface and use the struct via its constructor, not
109+
// the function type, to implement a single-method interface.
110+
type rateLimiterImpl struct {
111+
ReserveRateFunc
112+
}
113+
```
114+
115+
### 3. Use Constructors for Interface Values
116+
117+
Provide constructor functions rather than exposing concrete types. By
118+
default, each interface should provide a `func
119+
New<Type>(<Method1>Func, <Method2>Func, ...) Type` for all methods,
120+
using the concrete implementation struct. For example:
121+
122+
```go
94123
func NewRateReservation(wf WaitTimeFunc, cf CancelFunc) RateReservation {
95124
return rateReservationImpl{
96125
WaitTimeFunc: wf,
97126
CancelFunc: cf,
98127
}
99128
}
129+
130+
func NewRateLimiter(f ReserveRateFunc) RateLimiter {
131+
return rateLimiterImpl{ReserveRateFunc: f}
132+
}
100133
```
101134

102135
[The Go language automatically converts function literal
103136
expressions](https://go.dev/doc/effective_go#conversions) into the
104-
correct named type (i.e., `<Method>Func`), so we can write:
137+
correct named type (i.e., `<Method>Func`), so we can pass function
138+
literals to these constructors without an explicit conversion:
105139

106140
```go
107141
return NewRateReservation(
@@ -112,24 +146,10 @@ correct named type (i.e., `<Method>Func`), so we can write:
112146
)
113147
```
114148

115-
### 3. Use Constructors for Interface Values
116-
117-
Provide constructor functions rather than exposing concrete types:
118-
119-
```go
120-
// Good: Constructor function
121-
func NewRateLimiter(f ReserveRateFunc) RateLimiter {
122-
return rateLimiterImpl{ReserveRateFunc: f}
123-
}
124-
125-
// Avoid: Direct struct instantiation inside the package
126-
// rateLimiterImpl{ReserveRateFunc: f} // Don't do this, use the constructor
127-
```
128-
129-
This will help maintainers upgrade your callsites, should the
130-
interface gain a new method. For more complicated interfaces, this
131-
pattern can be combined with the Functional Option pattern in Golang,
132-
shown in the next example.
149+
For more complicated interfaces, this pattern can be combined with the
150+
[Functional Option
151+
pattern](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html)
152+
in Golang, shown in the next example.
133153

134154
Taken from `receiver/receiver.go`, here we setup signal-specific
135155
functions using a functional-option argument passed to
@@ -147,7 +167,7 @@ func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOpt
147167
// Accept options to configure various aspects of the interface
148168
func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory {
149169
f := factoryImpl{
150-
Factory: component.NewFactoryImpl(cfgType.Self, createDefaultConfig),
170+
Factory: component.NewFactory(cfgType.Self, createDefaultConfig),
151171
}
152172
for _, opt := range options {
153173
opt.applyOption(&f, cfgType)
@@ -156,7 +176,46 @@ func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefa
156176
}
157177
```
158178

159-
### 4. Constant-value Function Implementations
179+
### 4. Seal Public Interface Types
180+
181+
Using an unexported method "seals" the interface type so external
182+
packages can only use, not implement the interface. This allows
183+
interfaces to evolve safely because users are forced to use
184+
constructor functions.
185+
186+
```go
187+
type RateLimiter interface {
188+
ReserveRate(context.Context, int) (RateReservation, error)
189+
190+
private() // Prevents external implementations
191+
}
192+
```
193+
194+
This practice enables safely evolving interfaces. A new method can be
195+
added to a public interface type because public constructor functions
196+
force the user to obtain the new type and the new type is guaranteed
197+
to implement the old interface. If the functional option pattern is
198+
already being used, then new interface methods will need no new
199+
constructors, otherwise backwards compatibility can be maintained by
200+
adding new constructors, for example:
201+
202+
```go
203+
type RateLimiter interface {
204+
// Original method
205+
ReserveRate(context.Context, int) RateReservation
206+
207+
// New method (optional support)
208+
ExtraFeature()
209+
}
210+
211+
// Original constructor
212+
func NewRateLimiter(f ReserveRateFunc) RateLimiter { ... }
213+
214+
// New constructor
215+
func NewRateLimiterWithExtraFeature(rf ReserveRateFunc, ef ExtraFeatureFunc) RateLimiter { ... }
216+
```
217+
218+
### 5. Constant-value Function Implementations
160219

161220
For types defined by simple values, especially for enumerated types,
162221
define a `Self()` method to act as the corresponding functional
@@ -180,18 +239,22 @@ func (f TypeFunc) Type() Type {
180239
```
181240

182241
For example, we can decompose, modify, and recompose a
183-
`component.Factory`:
242+
`component.Factory` easily using Self methods to capture the
243+
constant-valued Type and Config functions:
184244

185245
```go
186-
// Construct a factory with a new default Config:
187-
factory := sometype.NewFactory()
188-
cfg := factory.CreateDefaultConfig()
189-
// ... Modify the config object
190-
// Pass cfg.Self as the default config function.
246+
// Construct a factory, get its default default Config:
247+
originalFactory := somepackage.NewFactory()
248+
cfg := originalFactory.CreateDefaultConfig()
249+
250+
// ... Modify the config object somehow
251+
252+
// Pass cfg.Self as the default config function,
253+
// return a new factory using the modified config.
191254
return NewFactoryImpl(factory.Type().Self, cfg.Self)
192255
```
193256

194-
## Rationale
257+
## Examples
195258

196259
### Flexibility and Composition
197260

@@ -215,8 +278,7 @@ func NewLimitedFactory(fact receiver.Factory, cfgf LimiterConfigurator) receiver
215278
}
216279
```
217280

218-
This is sometimes called aspect-oriented programming, for example it
219-
is easy to add logging to an existing function:
281+
For example, it is easy to add logging to an existing function:
220282

221283
```go
222284
func addLogging(f ReserveRateFunc) ReserveRateFunc {
@@ -226,112 +288,3 @@ func addLogging(f ReserveRateFunc) ReserveRateFunc {
226288
}
227289
}
228290
```
229-
230-
### Safe Interface Evolution
231-
232-
Using a private method allows sealing the interface type, which forces
233-
external users to use functional constructor methods. This allows
234-
interfaces to evolve safely because users are forced to use
235-
constructor functions, and instead of breaking stability for function
236-
definitions, we can add alternative constructors.
237-
238-
```go
239-
type RateLimiter interface {
240-
ReserveRate(context.Context, int) (RateReservation, error)
241-
242-
// Can add new methods without breaking existing code
243-
private() // Prevents external implementations
244-
}
245-
```
246-
247-
### Enhanced Testability
248-
249-
Individual methods can be tested independently:
250-
251-
```go
252-
func TestWaitTimeFunction(t *testing.T) {
253-
waitFn := WaitTimeFunc(func() time.Duration { return time.Second })
254-
assert.Equal(t, time.Second, waitFn.WaitTime())
255-
}
256-
```
257-
258-
## Implementation Guidelines
259-
260-
### 1. Interface Design
261-
262-
- Define interfaces with clear method signatures
263-
- Include a `private()` method if you need to control implementations
264-
- Keep interfaces focused and cohesive
265-
266-
### 2. Function Type Naming
267-
268-
- Use `<MethodName>Func` naming convention
269-
- Ensure function signatures match the interface method exactly
270-
- Always implement the corresponding interface method on the function type
271-
272-
### 3. Nil Handling
273-
274-
Always handle nil function types gracefully to act as a no-op implementation.
275-
276-
```go
277-
func (f ReserveRateFunc) ReserveRate(ctx context.Context, value int) (RateReservation, error) {
278-
if f == nil {
279-
return NewRateReservationImpl(nil, nil), nil // No-op implementation
280-
}
281-
return f(ctx, value)
282-
}
283-
```
284-
285-
### 4. Constructor Patterns
286-
287-
Follow consistent constructor naming for building an implementation
288-
of each interface:
289-
290-
```go
291-
// For interfaces: New<InterfaceName>Impl
292-
func NewRateLimiterImpl(f ReserveRateFunc) RateLimiter
293-
```
294-
295-
Follow consistent type naming for each method-function type:
296-
297-
```go
298-
// For function types: <MethodName>Func
299-
type ReserveRateFunc func(context.Context, int) (RateReservation, error)
300-
```
301-
302-
### 5. Implementation Structs
303-
304-
- Use unexported names for implementation structs
305-
- Embed function types directly
306-
- Implement private methods for sealing the interface
307-
308-
```go
309-
type RateLimiter interface {
310-
// ...
311-
312-
// Must use functional constructors outside this package
313-
private()
314-
}
315-
316-
type rateLimiterImpl struct {
317-
ReserveRateFunc
318-
}
319-
320-
func (rateLimiterImpl) private() {}
321-
```
322-
323-
## When to Use This Pattern
324-
325-
### Appropriate Use Cases
326-
327-
- **Interfaces with single or multiple methods** that benefit from composition
328-
- **Cross-cutting concerns** that need to be applied selectively
329-
- **Interface evolution** where you need to add methods over time
330-
- **Factory patterns** where you're assembling behavior from components
331-
- **Middleware/decorator scenarios** where you're transforming behavior
332-
333-
### When to Avoid
334-
335-
- **Stateful objects** where methods need to share significant state
336-
- **Performance-critical code** where function call overhead matters
337-
- **Simple implementations** where the pattern adds unnecessary complexity

0 commit comments

Comments
 (0)