Disadvantages of config injection

As we have seen, config injection can be used with both constructors and functions, It is, therefore, possible to build a system with only config injection. Unfortunately, config injection does have some disadvantages.

Passing config instead of abstract dependencies leaks implementation details—Consider the following code:

type PeopleFilterConfig interface {
DSN() string
}

func PeopleFilter(cfg PeopleFilterConfig, filter string) ([]Person, error) {
// load people
loader := &PersonLoader{}
people, err := loader.LoadAll(cfg)
if err != nil {
return nil, err
}

// filter people
out := []Person{}
for _, person := range people {
if strings.Contains(person.Name, filter) {
out = append(out, person)
}
}

return out, nil
}

type PersonLoaderConfig interface {
DSN() string
}

type PersonLoader struct{}

func (p *PersonLoader) LoadAll(cfg PersonLoaderConfig) ([]Person, error) {
return nil, errors.New("not implemented")
}

In this example, the PeopleFilter function is aware of the fact that PersonLoader is a database. This might not seem like a big deal, and if the implementation strategy never changes, it will have no adverse impact. Should we shift from a database to an external service or anything else, however, we would then have to change our PersonLoader database as well. A more future-proof implementation would be as follows:

type Loader interface {
LoadAll() ([]Person, error)
}

func PeopleFilter(loader Loader, filter string) ([]Person, error) {
// load people
people, err := loader.LoadAll()
if err != nil {
return nil, err
}

// filter people
out := []Person{}
for _, person := range people {
if strings.Contains(person.Name, filter) {
out = append(out, person)
}
}

return out, nil
}

This implementation is unlikely to require changes should we change where our data is loaded from.

Dependency life cycles are less predictable—In the advantages, we stated that dependency creation can be deferred until use. Your inner critic may have rebelled against that assertion, and for a good reason. It is an advantage, but it also makes the life cycle of the dependency less predictable. When using constructor injection or method injection, the dependency must exist before it is injected. Due to this, any issues with the creation or initialization of the dependency surfaces at this earlier time. When the dependency is initialized at some unknown later point, a couple of issues can arise.

Firstly, if the issue is unrecoverable or causes the system to panic, this would mean the system initially seems healthy and then becomes unhealthy or crashes unpredictably. This unpredictability can lead to issues that are extremely hard to debug.

Secondly, if the initialization of the dependency includes the possibility of a delay, we have to be aware of, and account for, any such delay. Consider the following code:

func DoJob(pool WorkerPool, job Job) error {
// wait for pool
ready := pool.IsReady()

select {
case <-ready:
// happy path

case <-time.After(1 * time.Second):
return errors.New("timeout waiting for worker pool")
}

worker := pool.GetWorker()
return worker.Do(job)
}

Now compare this to an implementation that assumes the pool is ready before injection:

func DoJobUpdated(pool WorkerPool, job Job) error {
worker := pool.GetWorker()
return worker.Do(job)
}

What would happen if this function were a part of an endpoint with a latency budget? If the startup delay is greater than the latency budget, then the first request would always fail.

Over-use degrades the UX—While I strongly recommended that you only use this pattern for configuration and environmental dependencies such as instrumentation, it is possible to apply this pattern in many other places. By pushing the dependencies into a config interface, however, they become less apparent, and we have a larger interface to implement. Let's re-examine an earlier example:

// NewByConfigConstructor is the constructor for MyStruct
func NewByConfigConstructor(cfg MyConfig, limiter RateLimiter, cache Cache) *MyStruct {
return &MyStruct{
// code removed
}
}

Consider the rate limiter dependency. What happens if we merge that into the Config interface? It becomes less apparent that this object uses and relies on a rate limiter. If every similar function has rate limiting, then this will be less of a problem as its usage becomes more environmental.

The other less visible aspect is configuration. The configuration of the rate limiter is likely not consistent across all usages. This is a problem when all of the other dependencies and config are coming from a shared object. We could compose the config object and customize the rate limiter returned, but this feels like over-engineering.

Changes can ripple through the software layers This issue only applies when the config passed through the layers. Consider the following example:

func NewLayer1Object(config Layer1Config) *Layer1Object {
return &Layer1Object{
MyConfig: config,
MyDependency: NewLayer2Object(config),
}
}

// Configuration for the Layer 1 Object
type Layer1Config interface {
Logger() Logger
}

// Layer 1 Object
type Layer1Object struct {
MyConfig Layer1Config
MyDependency *Layer2Object
}

// Configuration for the Layer 2 Object
type Layer2Config interface {
Logger() Logger
}

// Layer 2 Object
type Layer2Object struct {
MyConfig Layer2Config
}

func NewLayer2Object(config Layer2Config) *Layer2Object {
return &Layer2Object{
MyConfig: config,
}
}

With this structure, when we need to add a new configuration or dependency to the Layer2Config interface, we would also be forced to add it to the Layer1Config interface. Layer1Config would then be in violation of the interface segregation principle as discussed in Chapter 2SOLID Design Principles for Go, which indicates that we might have a problem. Additionally, depending on the code's layering and level of reuse, the number of changes could be significant. In this case, a better option would be to apply constructor injection to inject Layer2Object into Layer1Object. This would completely decouple the objects and remove the need for the layered changes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.79.33