As Alan Perlis once said: The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information

Let’s explore this issue with some code extracted from an OSS project Broadway Namely’s deployment tool for staging environments.

Broadway has the concept of an instance which is the data representation of what the system uses to deploy services into Kubernetes.

Here is the definition of that data:

type Instance struct {
  PlaybookID string
  ID string
  Status
}

func (i *Instance) Path() string {
  return fmt.Sprintf("%s/instances/%s/%s", env.EtcdPath, i.PlaybookID, i.ID)
}

The main issue with this implementation is duplication. If a programmer needs a Path for others use cases he will have to replicate the same string formatting every time. The problem with this is the duplication of information.

The don’t repeat yourself principle states to have only one representation of any concept in the system; abstractions like this make new concepts evident.

How can we solve that issue? One approach would be to create a function to do the formatting, like so:

func BuildPath(PlaybookID, ID string) string {
  return fmt.Sprintf("%s/instances/%s/%s", env.EtcdPath, PlaybookID, ID)
}

And that will reduce the coupling between the data and the behavior. However if we take a look a little bit closer we are actually hiding is an important concept from the domain which is a Path.

How do we know that we are hiding an important concept from the system? To answer that question let’s take a look to another code example:

func ThatExpectAPath(path string) {
  // do something here with a Path
}

The issue here is that the function is expecting a string as an argument not specifically a Path type which is the actual concept that this function needs. The reason why this is an issue is because you could pass any string and the compiler won’t complain. Wouldn’t be better if you have a safety net in place to force you to look the proper way for formatting a Path? Well you have it since you are using an static typed language. The only piece missing is the new type definition which for this particular case at least the naming is evident; let’s see this in action:

type Path struct {
  PlaybookID string
  ID string
  RootPath string
}

func (p *Path) String() string {
  return fmt.Sprintf("%s/instances/%s/%s", p.RootPath, p.PlaybookID, p.ID)
}

func ThatExpectAPath(path Path) {
  // do something here with a Path
}

The two main benefits of this approach are:

  • Separation of concerns, now every time that we want to change something in the system we won’t need to hunt down for different ways for creating a new Path we are going to look at the type and do what we need according to that definition.

  • Higher level of abstraction, the only way that we humans have to avoid complexity is to work on a higher level of abstraction. We can get more done by combining different components rather than manipulating variables and control flows.

What about creating the wrong abstraction? The answer for that one is easy; just rollback into the old code and start over. It will be easier for the client to remove a type and start with duplication than to hunt that code down and remove it when there’s no obvious starting point.

How does this small piece fit with the rest of the system? Now we can use the embedded type mechanism of Go and compose our old type.

type Instance struct {
  ...
  Path
}

With that in place we can keep using the same formatting across the system and have the safety net of the compiler when passing that concept around.

By introducing this custom type in the system we decrease the translation cost between business concepts and implementation details making it easier for developers to compose better abstractions with smaller reusable components.