Loose Coupling in Go lang

Go lang offers a wide range of features that help us achieve loosely coupled program designs. Coupling refers to the direct knowledge that an element of a system has of another. Loose Coupling means interconnecting components without assumptions bleeding through our system. In this blog post I will talk about a couple of features of Go that help us implement this programming approach: interfaces and higher-order functions.

1. Interfaces

In Go we can define a type interface by supplying a set of method definitions:

type /* name of interface */ interface {
     // method definitions, that is without implementation
     // e.g. sum(a, b int) int
}

Let's say we're working on a chess program, and we want to define an interface to present the game in multiple I/O devices. We need a method to get the input from the user, and another one to present the output.

type IO interface {
    Read() string
    Write(message string)
}

When we call Read we expect a string back, and when we call Write we pass in a message. In Go, any value that implements those two functions satisfies the interface implicitly, i.e. there's no implements required or anything of the like. For example:

type CommandLine struct{}

func (cli CommandLine) Read() string {
    var input string

    fmt.Scanf("%s\n", &input)

    return input
}

func (cli CommandLine) Write(output string) {
    fmt.Printf("%v", output)
}

This is especially useful for testing. We can create a FakeIO type and have complete control over its input and output to verify their contents.

type FakeIO struct {
    Input string
    Output string
}

func (io FakeIO) Read() string {
    return io.Input
}

func (io FakeIO) Write(output string) {
    io.Output = output
}

Now we can stub and spy calls to I/O in our tests. Suppose we want the player to enter his name when we start a new game:

func (player Player) GetName(io IO) {
     io.Write("What's your name?")
     player.Name = io.Read()
}

func TestGetPlayerName(t *testing.T) {
    var io FakeIO

    io.Input = "Javier"
    player.GetName(io)

    if name := player.Name; name != "Javier" {
        t.Errorf("Expected name to be %#v, but was %#v",
            "Javier", name)
    }
}

And it's just as easy to verify the game output—we'd just check the value of io.Output when testing functions that use io.Write(string).

Interfaces create a common set of messages that our program expects. As long as our values respond to those messages, we can safely swap out elements of the system. In this example we used this approach to our advantage to create unit tests, but imagine if we wanted to create a GUI for our chess game instead of using the command line. As long as our GUI type responds to Read() and Write(), our code won't have to change, and Go makes it really simple to implement interfaces.

2. Higher-order functions

In mathematics and computer science, a higher-order function (also functional form, functional or functor) is a function that does at least one of the following:

  • takes one or more functions as an input
  • outputs a function

Go has static types, which can complicate the use of functions as a means of injecting dependencies. Continuing with our chess game, consider the problem of playing against the computer.

When it's the human player's turn, we have to pass the chess board and the IO interface to get the move input, which could be a mouse click in a GUI, or text entered through the command line, etc.:

player.Move(io, chessBoard)

The computer, on the other hand, has no need for an IO interface to supply its move. It calculates what to do based on the current state of the chess board.

computer.Move(chessBoard)

An implementation of the game loop could look like this:

for !chessBoard.HasCheckMate() {
    player.Move(io, chessBoard)
    computer.Move(chessBoard)
}

As you may notice, the problem with the loop above is that we're not checking for the end game condition before the computer moves. If the human wins, the computer will still attempt to move. This can be easily solved if we create an endless loop instead, and guard each player move with HasCheckMate().

for {
    if !chessBoard.HasCheckMate() {
        player.Move(io, chessBoard)
    }

    if !chessBoard.HasCheckMate() {
        computer.Move(chessBoard)
    }
}

But I'd rather avoid duplication (consider what happens if we want to add a third player, and a fourth, and so on).

It'd be nice to store the players in a data structure, and cycle through it to get the player for each turn. Then we'd be able to write something like this:

for !chessBoard.HasCheckMate() {
    getCurrentPlayer().Move(...)
}

Storing the references to the players wouldn't be enough, though, because of the different arity in their Move functions. If we really wanted the function above to work there are a few approaches we could try, but I won't be doing that in this blog post. Instead, I'm going to use Go's anonymous functions to store the Move functions, not the players.

var playerMove = func(chessBoard chess.Board) {
    player.Move(io, chessBoard)
}

var computerMove = func(chessBoard chess.Board) {
    computer.Move(chessBoard)
}

var moves = []func(chessBoard chess.Board) {
    playerMove,
    computerMove,
}

The anonymous functions receive only the chess board, so arity is no longer an issue and we can store them in an array.

Finally our game loop can look like this:

for !chessBoard.HasCheckMate() {
    moveCurrentPlayer(chessBoard)
    changeTurn()
}

And calling move on the current player is trivial:

func moveCurrentPlayer(chessBoard chess.Board) {
    moves[0](chessBoard)
}

To change the player turn, simply cycle through the contents of the moves array.

Higher-order functions allowed us to make different interfaces speak in the same terms. Now we can add more players without changing the game logic. The arity of their Move function wouldn't matter because we're using anonymous functions to wrap them. We know that the argument they all will have in common is the chess board.

These are only some of the language features that we can use to achieve loosely coupled designs. In this example we used interfaces and higher-order functions to comply with the static types constraints while keeping our code concise and easy to test.

On an ending note, I find Go lang very refreshing. Out of the box it's simple and powerful. It provides just enough utility functions (through its src packages), so you don't have to reinvent the wheel. It treats functions as first-class citizens, and that's my favorite feature of the language so far.

Javier Trevino Saldana, Software Craftsman

Javier Trevino Saldana is a software professional who strives to help companies and teams to become better.

Interested in 8th Light's services? Let's talk.

Contact Us