For people, tight coupling might be a good thing. For Go code, it's really not. Coupling is a measure of how objects relate to or depend on each other. When the tight coupling is present, this interdependence forces the objects or packages to evolve together, adding complexity and maintenance costs.
Coupling-related smells are perhaps the most insidious and obstinate but by far the most rewarding when dealt with. They are often the result of a lack of object-oriented design or insufficient use of interfaces.
Sadly, I don't have a handy tool to help you find these smells but I am confident that, by the end of this book, you will have no trouble spotting and dealing with them.
Frequently, I find it useful to implement a feature in a tightly coupled form first and then work backward to decouple and thoroughly unit test my code before submitting it. For me, it is especially helpful in cases where the correct abstractions are not obvious.
These smells include the following:
- Dependence on God objects: These are large objects that know too much or do too much. While this is a general code smell and something that should be avoided like the plague, the problem from a DI perspective is that too much of the code is dependent on this one object. When they exist and we are not careful, it won't be long before Go will be refusing to compile due to a circular dependency. Interestingly, Go considers dependencies and imports not at an object level but at a package level. So we have to avoid God packages as well. We will address a very common God object problem in Chapter 8, Dependency Injection by Config.
- Circular dependencies: These are where package A depends on package B, and package B depends on package A. This is an easy mistake to make and sometimes a hard one to get rid of.
In the following example, while the config is arguably a God object and therefore a code smell, I am hard pressed to find a better way to import the config from a single JSON file. Instead, I would argue that the problem to be solved is the use of the config package by orders package. A typical config God object follows:
package config
import ...
// Config defines the JSON format of the config file
type Config struct {
// Address is the host and port to bind to.
// Default 0.0.0.0:8080
Address string
// DefaultCurrency is the default currency of the system
DefaultCurrency payment.Currency
}
// Load will load the JSON config from the file supplied
func Load(filename string) (*Config, error) {
// TODO: load currency from file
return nil, errors.New("not implemented yet")
}
In the attempted usage of the config package, you can see that the Currency type belongs to the Package package and so including it in config, as shown in the preceding example, causes a circular dependency:
package payment
import ...
// Currency is custom type for currency
type Currency string
// Processor processes payments
type Processor struct {
Config *config.Config
}
// Pay makes a payment in the default currency
func (p *Processor) Pay(amount float64) error {
// TODO: implement me
return errors.New("not implemented yet")
}
- Object orgy: These occur when an object has too much knowledge of and/or access to the internals of another or, to put it another way, insufficient encapsulation between objects. Because the objects are joined at the hip, they will frequently have to evolve together, increasing the cost of understanding the code and maintaining it. Consider the following code:
type PageLoader struct {
}
func (o *PageLoader) LoadPage(url string) ([]byte, error) {
b := newFetcher()
// check cache
payload, err := b.cache.Get(url)
if err == nil {
// found in cache
return payload, nil
}
// call upstream
resp, err := b.httpClient.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// extract data from HTTP response
payload, err = ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// save to cache asynchronously
go func(key string, value []byte) {
b.cache.Set(key, value)
}(url, payload)
// return
return payload, nil
}
type Fetcher struct {
httpClient http.Client
cache *Cache
}
In this example, PageLoader repeatably calls the member variable of the Fetcher. So much so that, if the implementation of Fetcher changed, it's highly likely that PageLoader would be affected. In this case, these two objects should be merged together as PageLoader has no extra functionality.
- Yo-yo problem: The standard definition of this smell is when the inheritance graph is so long and complicated that the programmer has to keep flipping through the code to understand it. Given that Go doesn't have inheritance, you would think we would be safe from this problem. However, it is possible if you try hard enough, with excessive composition. To address this issue, it's better to keep relationships as shallow and abstract as possible. In this way, we can concentrate on a much smaller scope when making changes and compose many small objects into a larger system.
- Feature envy: When a function makes extensive use of another object, it is envious of it. Typically, an indication that the function should be moved away from the object it is envious of. DI may not be the solution to this, but this smell does indicate high coupling and, therefore, is an indicator to consider applying DI techniques:
func doSearchWithEnvy(request searchRequest) ([]searchResults, error) {
// validate request
if request.query == "" {
return nil, errors.New("search term is missing")
}
if request.start.IsZero() || request.start.After(time.Now()) {
return nil, errors.New("start time is missing or invalid")
}
if request.end.IsZero() || request.end.Before(request.start) {
return nil, errors.New("end time is missing or invalid")
}
return performSearch(request)
}
func doSearchWithoutEnvy(request searchRequest) ([]searchResults, error) {
err := request.validate()
if err != nil {
return nil, err
}
return performSearch(request)
}
As your code becomes less coupled, you will find the individual parts (packages, interfaces, and structs) will become more focused. This is referred to as having high cohesion. Both low coupling and high cohesion are desirable as they make the code easier to understand and work with.