Service locator

First, a definition—Service locator is a software design pattern that revolves around an object that acts as a central repository of all dependencies and is able to return them by name. You'll find this pattern in use in many languages and at the heart of some DI frameworks and containers.

Before we dig into why this is DI induced damage, let's look at an example of an overly simplified service locator:

func NewServiceLocator() *ServiceLocator {
return &ServiceLocator{
deps: map[string]interface{}{},
}
}

type ServiceLocator struct {
deps map[string]interface{}
}

// Store or map a dependency to a key
func (s *ServiceLocator) Store(key string, dep interface{}) {
s.deps[key] = dep
}

// Retrieve a dependency by key
func (s *ServiceLocator) Get(key string) interface{} {
return s.deps[key]
}

In order to use our service locator, we first have to create it and map our dependencies with their names, as shown in the following example:

// build a service locator
locator := NewServiceLocator()

// load the dependency mappings
locator.Store("logger", &myLogger{})
locator.Store("converter", &myConverter{})

With our service locator built and dependencies set, we can now pass it around and extract dependencies as needed, as shown in the following code:

func useServiceLocator(locator *ServiceLocator) {
// use the locators to get the logger
logger := locator.Get("logger").(Logger)

// use the logger
logger.Info("Hello World!")
}

Now, if we wanted to swap out the logger for a mock one during testing, then we would only have to construct a new service locator with the mock logger and pass it into our function.

So what is wrong with that? Firstly, our service locator is now a God object (as mentioned in Chapter 1, Never Stop Aiming for Better) that we would likely end up passing around all over the place. It might sound like a good thing to only have to pass one object into every function but it leads to the second issue.

The relationship between an object and the dependencies it uses is now completely hidden from the outside. We are no longer able to look at a function or struct definition and immediately know what dependencies are required.

And lastly, we are operating without the protection of Go's type system and compiler. In the previous example, the following line might have caught your attention:

logger := locator.Get("logger").(Logger)

Because the service locator accepts and returns interface{}, every time we need to access a dependency, we are required to cast into the appropriate type. This casting not only makes the code messier, it can cause runtime crash if the value is missing or of the wrong type. We can account for these issues with yet more code, as shown in the following example:

// use the locators to get the logger
loggerRetrieved := locator.Get("logger")
if loggerRetrieved == nil {
return
}
logger, ok := loggerRetrieved.(Logger)
if !ok {
return
}

// use the logger
logger.Info("Hello World!")

With the previous approach, our application will no longer crash, but it's getting rather messy.

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

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