Skip to content

Commit 2ca19e5

Browse files
committed
more text up front
1 parent 35668a1 commit 2ca19e5

File tree

1 file changed

+337
-0
lines changed

1 file changed

+337
-0
lines changed
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# Functional Composition Pattern
2+
3+
## Overview
4+
5+
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.
11+
12+
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.
16+
17+
For every method in the public interface, a corresponding `type
18+
<Method>Func func(...) ...` declaration in the same package will
19+
exist, having the matching signature.
20+
21+
For every interface type, there is a corresponding functional
22+
constructor `func New<Type>(<Method1>Func, <Method2>Func, ...) Type`
23+
in the same package for constructing a functional composition of
24+
interface methods.
25+
26+
Interface stability for exported interface types is our primary
27+
objective. The Functional Composition pattern supports safe interface
28+
evolution, first by "sealing" the type with an unexported interface
29+
method. This means all implementations of an interface must use
30+
constructors provided in the package.
31+
32+
These "sealed concrete" implementation objects support adding new
33+
methods in future releases, _without changing the major version
34+
number_, because public interface types are always provided through a
35+
package-provided implementation. As a key requirement, every function
36+
must have a simple "no-op" implementation correspodning with the zero
37+
value of the `<Method>Func`.
38+
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+
42+
## Key concepts
43+
44+
### 1. Decompose Interfaces into Function Types
45+
46+
Instead of implementing interfaces directly on structs, we create
47+
function types for each method:
48+
49+
```go
50+
// Interface definition
51+
type RateReservation interface {
52+
WaitTime() time.Duration
53+
Cancel()
54+
}
55+
56+
// Function types for each method
57+
type WaitTimeFunc func() time.Duration
58+
type CancelFunc func()
59+
60+
// Function types implement their corresponding methods
61+
func (f WaitTimeFunc) WaitTime() time.Duration {
62+
if f == nil {
63+
return 0
64+
}
65+
return f()
66+
}
67+
68+
func (f CancelFunc) Cancel() {
69+
if f == nil {
70+
return
71+
}
72+
f()
73+
}
74+
```
75+
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.
82+
83+
### 2. Compose Functions into Interface Implementations
84+
85+
Create concrete implementations embed the corresponding function type:
86+
87+
```go
88+
type rateReservationImpl struct {
89+
WaitTimeFunc
90+
CancelFunc
91+
}
92+
93+
// Constructor for an instance
94+
func NewRateReservation(wf WaitTimeFunc, cf CancelFunc) RateReservation {
95+
return rateReservationImpl{
96+
WaitTimeFunc: wf,
97+
CancelFunc: cf,
98+
}
99+
}
100+
```
101+
102+
[The Go language automatically converts function literal
103+
expressions](https://go.dev/doc/effective_go#conversions) into the
104+
correct named type (i.e., `<Method>Func`), so we can write:
105+
106+
```go
107+
return NewRateReservation(
108+
// Wait time 1 second
109+
func() time.Duration { return time.Second },
110+
// Cancel is a no-op.
111+
nil,
112+
)
113+
```
114+
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.
133+
134+
Taken from `receiver/receiver.go`, here we setup signal-specific
135+
functions using a functional-option argument passed to
136+
`receiver.NewFactory`:
137+
138+
```go
139+
// Setup optional signal-specific functions (e.g., logs)
140+
func WithLogs(createLogs CreateLogsFunc, sl component.StabilityLevel) FactoryOption {
141+
return factoryOptionFunc(func(o *factoryImpl, cfgType component.Type) {
142+
o.CreateLogsFunc = createLogs
143+
o.LogsStabilityFunc = sl.Self
144+
})
145+
}
146+
147+
// Accept options to configure various aspects of the interface
148+
func NewFactory(cfgType component.Type, createDefaultConfig component.CreateDefaultConfigFunc, options ...FactoryOption) Factory {
149+
f := factoryImpl{
150+
Factory: component.NewFactoryImpl(cfgType.Self, createDefaultConfig),
151+
}
152+
for _, opt := range options {
153+
opt.applyOption(&f, cfgType)
154+
}
155+
return f
156+
}
157+
```
158+
159+
### 4. Constant-value Function Implementations
160+
161+
For types defined by simple values, especially for enumerated types,
162+
define a `Self()` method to act as the corresponding functional
163+
constant:
164+
165+
```go
166+
// Self returns itself.
167+
func (t Type) Self() Type {
168+
return t
169+
}
170+
171+
// TypeFunc is ...
172+
type TypeFunc func() Type
173+
174+
// Type gets the type of the component created by this factory.
175+
func (f TypeFunc) Type() Type {
176+
if f == nil {
177+
}
178+
return f()
179+
}
180+
```
181+
182+
For example, we can decompose, modify, and recompose a
183+
`component.Factory`:
184+
185+
```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.
191+
return NewFactoryImpl(factory.Type().Self, cfg.Self)
192+
```
193+
194+
## Rationale
195+
196+
### Flexibility and Composition
197+
198+
This pattern enables composition scenarios by making it easy to
199+
compose and decompose interface values. For example, to wrap a
200+
`receiver.Factory` with a limiter of some sort:
201+
202+
```go
203+
// Transform existing factories with cross-cutting concerns
204+
func NewLimitedFactory(fact receiver.Factory, cfgf LimiterConfigurator) receiver.Factory {
205+
return receiver.NewFactoryImpl(
206+
fact.Type,
207+
fact.CreateDefaultConfig,
208+
receiver.CreateTracesFunc(limitReceiver(fact.CreateTraces, traceTraits{}, cfgf)),
209+
fact.TracesStability,
210+
receiver.CreateMetricsFunc(limitReceiver(fact.CreateMetrics, metricTraits{}, cfgf)),
211+
fact.MetricsStability,
212+
receiver.CreateLogsFunc(limitReceiver(fact.CreateLogs, logTraits{}, cfgf)),
213+
fact.LogsStability,
214+
)
215+
}
216+
```
217+
218+
This is sometimes called aspect-oriented programming, for example it
219+
is easy to add logging to an existing function:
220+
221+
```go
222+
func addLogging(f ReserveRateFunc) ReserveRateFunc {
223+
return func(ctx context.Context, n int) (RateReservation, error) {
224+
log.Printf("Reserving rate for %d", n)
225+
return f(ctx, n)
226+
}
227+
}
228+
```
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)