How-to fix tightly coupled Go code

Tightly coupled Go code

Have you ever added a new feature only to have another one break?  After fixing the break, something else breaks, like some kind of bug whack-a-mole?

Have you ever spent hours debugging a broken test only to find the issue lurking in another, supposedly unrelated package?

These issues are caused by code being tightly coupled.  

In this article, we are going to examine ways to make code easier to understand, maintain and test by using decoupling.

What is coupling?

In software, coupling is the measure of how much two parts (objects, packages, functions) depend on each other.

Take the following example:

type Config struct {
	DSN            string
	MaxConnections int
	Timeout        time.Duration
}

type PersonLoader struct {
	Config *Config
}

These two objects cannot exist without each other.  As such they are considered to be tightly coupled.

Why is tightly coupled code a problem?

There are many adverse effects of tightly coupled code, but perhaps the most significant is the fact that it causes shotgun surgery. Shotgun surgery is the term used to describe a situation where a change in one part of the code causes or necessitates a log of small, related changes elsewhere in the code.

Consider the following code:

func GetUserEndpoint(resp http.ResponseWriter, req *http.Request) {
	// get and check inputs
	ID, err := getRequestedID(req)
	if err != nil {
		resp.WriteHeader(http.StatusBadRequest)
		return
	}

	// load requested data
	user, err := loadUser(ID)
	if err != nil {
		// technical error
		resp.WriteHeader(http.StatusInternalServerError)
		return
	}
	if user == nil {
		// user not found
		resp.WriteHeader(http.StatusNoContent)
		return
	}

	// prepare output
	switch req.Header.Get("Accept") {
	case "text/csv":
		outputAsCSV(resp, user)

	case "application/xml":
		outputAsXML(resp, user)

	case "application/json":
		fallthrough

	default:
		outputAsJSON(resp, user)
	}
}

Now consider what happens if we were to add a password field to the User object.  Let’s assume we don’t want the field to be output as part of the API response.  We would then have to introduce additional code to our outputAsCSV(), outputAsXML() and outputAsJSON() methods.

This all seems rational and reasonable, but what happens if we also had another endpoint that also includes the User type as part of its output, like a “Get All Users” endpoint.  We’d have to make similar changes there too. This is caused by the fact that the “Get User” endpoint is tightly coupled with the output rendering of the User type.

On the other hand, if we were to move the rendering logic from the GetUserEndpoint() to User type then we would only have one place to make changes.  And perhaps, more importantly, that place is obvious and easy to find as it is next to the location where we are adding the new field, thereby improving the maintainability of our code as a whole.

Before we take a deeper dive into how to fix tightly coupled code there is one last thing we should discuss, the Dependency Inversion Principle.

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is a term coined by Robert C. Martin in his 1996 article for the C++ Report titled “The Dependency Inversion Principle”.  He defines it as:

High level modules should not depend on low level modules. Both should depend on abstractions. Abstractions should not depend upon details. Details should depend on abstractions

Robert C. Martin

As with a lot of Robert C. Martin’s writing, there is a wealth of wisdom packed into just a few sentences.  The following is how I break it down (and translate it to Go):

1) High-level packages should not depend on low-level packages – When we construct a Go application, some packages are called from our main().  These can be considered high-level packages. Conversely, some packages interact with external resources, like a database, are typically not called from main() but rather from the business logic layer which is 1 or 2 levels lower down.   

This point asserts that high-level packages should not depend on low-level packages.  Instead of depending on these packages, which are essentially implementation details, high-level packages should depend on abstractions.  Thereby keeping them decoupled.

2) Structs* should not depend on Structs* – When a struct accepts another struct as a method input or member variable:

type PizzaMaker struct{}

func (p *PizzaMaker) MakePizza(oven *SuperPizaOven5000) {
	pizza := p.buildPizza()
	oven.Bake(pizza)
}

It is no longer possible to separate these two structs.  These objects are very tightly coupled and as a result not very flexible.   Consider this real-world example: let’s say I walk into the travel agent and say “Can I please have seat 15D on the 3.30 Thursday Qantas flight to Sydney?”  It will be extremely difficult for the travel agent to fulfill my request.

But if I loosen my requirements, as you do when you change your input parameter from a struct to an interface, to: “Can I please have a seat on any Thursday flight to Sydney?” then the travel agents life is more flexible, and I am more likely to get my seat.

Updating our example to adhere to this idea gives us:

type PizzaMaker struct{}

func (p *PizzaMaker) MakePizza(oven Oven) {
	pizza := p.buildPizza()
	oven.Bake(pizza)
}

type Oven interface {
	Bake(pizza Pizza)
}

Now we can use any object that implements the Bake() method.

3) Interfaces should not depend on Structs* – Similar to the previous point, this is about the specificity of the requirements.  If we define our interface as:

type Config struct {
	DSN            string
	MaxConnections int
	Timeout        time.Duration
}

type PersonLoader interface {
	Load(cfg *Config, ID int) *Person
}

Then we are coupling our PersonLoader with this very specific config struct. This means that any attempt to reuse the PersonLoader in a different project will require us to make some changes. The changes could also result in bugs. On the other hand, if we decouple PersonLoader from the Config like so:

type PersonLoaderConfig interface {
	DSN() string
	MaxConnections() int
	Timeout() time.Duration
}

type PersonLoader interface {
	Load(cfg PersonLoaderConfig, ID int) *Person
}

Now, we can reuse the PersonLoader without any changes.
(* – Structs above should be taken to mean structs that provide logic and/or implement interfaces and does not include structs that are used as Data Transfer Objects)

Fixing tightly coupled code

With all the background out of the way, let’s dive into a more meaty example of resolving tightly coupled code.

Our example begins with 2 objects, Person and BlueShoes in 2 different packages.  Like this:

As you can see they are tightly coupled; there is no way for the Person struct to exist without BlueShoes.

If you are like me and come from a Java/C++ (or similar) background, then your first instinct to decouple the objects would be to define an interface in the shoes package.  Resulting in:

In many languages and that would be the end of it.  However, for Go, we can decouple these objects even further.

Before we do that, we should also take note of another problem.

You may have noticed that our Person struct only implements a Walk() method, whereas Footwear implements both Walk() and Run().  This disparity leaves the relationship between Person and Footwear somewhat unclear and violates another one of Robert C. Martin’s principles called the Interface Segregation Principle (ISP), which states:

Clients should not be forced to depend on methods they do not use.

Robert C. Martin

Fortunately, we can resolve both of these issues by defining an interface in the people package instead of the shoes package like so:

Now, this might seem like a small matter and perhaps not worth your precious time.  But the difference is profound.

In this example, our 2 packages are now entirely decoupled.  There is no need for the people package to depend on or use the shoes package.

With this change, the requirements (API) of the person package is clear and concise and easy to find, as they are in the same package and finally, changes to the shoes package are less likely to influence the people package.

Final Thoughts

As I wrote in my book, Hands-On Dependency Injection in Go, one of the most prevalent concepts in Go language seems to be the Unix Philosophy which states:

“Write programs that do one thing and do it well. Write programs to work together.”

These concepts are everywhere in the Go standard libraries and even show up in language design decisions.   Decisions like having interfaces implemented implicitly (i.e. without “implements” keyword) enable us, the users of the language, to implement decoupled code that serves a singular purpose and composes easily.

Loosely coupled code is easy to understand as all the information you need is kept together in one place.  This, in turn, makes the code both easier to test and extend.

So next time you see a concrete object as a function parameter or member variable, ask yourself, “Is this necessary?”, “Would this be more flexible, understandable or maintainable if I changed it to an interface?

Happy Coding!


If you like this content and would like to be notified when there are new posts or would like to be kept informed regarding the upcoming book launch, please join my Google Group (very low traffic and no spam).