33## Overview
44
55This 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
1212When 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
1717For 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
3333methods in future releases, _ without changing the major version
3434number_ , because public interface types are always provided through a
3535package-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
3737value 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
6158func (f WaitTimeFunc ) WaitTime () time .Duration {
6259 if f == nil {
63- return 0
60+ return 0 // No-op behavior
6461 }
6562 return f ()
6663}
6764
6865func (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
8887type 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
94123func 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
103136expressions] ( 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
134154Taken from ` receiver/receiver.go ` , here we setup signal-specific
135155functions 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
148168func 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
161220For types defined by simple values, especially for enumerated types,
162221define a ` Self() ` method to act as the corresponding functional
@@ -180,18 +239,22 @@ func (f TypeFunc) Type() Type {
180239```
181240
182241For 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
222284func 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