Single Responsibility Principle in Go
After watching Dave Cheney’s talk about SOLID principles in Go, I wanted to jump into the discussion in a practical way; so I decided to solve an small problem and see how to apply at least one principle in this case SRP. The problem that I’m going to solve is to build a data model for for a deck of cards.
To make sure that we are going to move in the right path let’s try to TDD our way through this problem. TDD is a tool for designing software and as a side effect it provides us with the benefit of automatic correctness checking.
So, How do we start? Let’s define some requirements based on the Wikipedia article.
When dealing a new deck
I want to get 52 cards
When I put card in the deck
I want to see a rank and a suit for that card
I think this two requirements are good to start and we can always go back to our product owner and add more stories to the backlog.
Let’s implement the first requirement by writing a test. Before dealing the deck we need to create it; so let’s write that test.
package main_test
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewDeck(t *testing.T) {
t.Run("when creating a new deck", func(t *testing.T) {
deck := Deck{}
assert.Equal(t, len(deck.Cards), 52, "retrieve 52 cards")
})
}
A good practice for naming package for testing is to use the _test
by using
this nomenclature we will be force to be explicit about external dependencies
by exporting just what we need. Since the test is our first client it make a lot
of sense.
By running the test we get the following error:
main_test.go|14| deck.Cards undefined (type main.Deck has no field or method Cards)
The compiler is saying that we need to add a new Deck
struct with a field
name Cards
exposing the question - what is a card? The way to figure that out
is by adding a new test for it. If we take a closer look at our old test it doesn’t
say much about what makes a card be a card. Based on the wikipedia article we know
that a card consist of a rank and a suit. So let’s define that test.
t.Run("when creating a new deck", func(t *testing.T) {
deck := Deck{}
t.Run("it contains four aces of spades, clubs, diamonds and hearts", func(t *testing.T) {
aces := []Card{}
expectedCards := []Card{
Card{Rank: "ace", Suit: "hearts"},
Card{Rank: "ace", Suit: "diamonds"},
Card{Rank: "ace", Suit: "spades"},
Card{Rank: "ace", Suit: "clubs"},
}
for _, card := range deck.Cards {
if card.Rank == "ace" {
aces := append(aces, card)
}
}
assert.Equal(t, expectedCards, aces)
})
})
What we get by running this test is the following error:
main.go|4| undefined: Card
So let’s define the Card
type:
type Card struct {
Rank string
Suit string
}
After writing the production code the test is showing us our first assertion failure:
--- FAIL: TestNewDeck (0.00s)
--- FAIL: TestNewDeck/when_creating_a_new_deck (0.00s)
--- FAIL: TestNewDeck/when_creating_a_new_deck/it_contains_four_aces_of_spades,_clubs,_diamonds_and_hearts (0.00s)
Error Trace: ns.go:225:
Error: Not equal: []main.Card{main.Card{Rank:"ace", Suit:"hearts"}, main.Card{Rank:"ace", Suit:"diamonds"}, main.Card{Rank:"ace", Suit:"spades"}, main.Card{Rank:"ace", Suit:"clubs"}} (expected)
Since we need a way to build the inner state of the deck at this point is a good idea
to introduce a proper constructor for the Deck
to handle the initial state of
the data:
var (
ranks = []string{"ace"}
suits = []string{"hearts", "diamonds", "spades", "clubs"}
)
type Card struct {
Rank string
Suit string
}
type Deck struct {
Cards []Card
}
func NewDeck() Deck {
cards := []Card{}
for _, rank := range ranks {
for _, suit := range suits {
cards = append(cards, Card{Rank: rank, Suit: suit})
}
}
return Deck{Cards: cards}
}
After making our first test green let’s see if we can finish the TDD loop cycle with a refactor. Our rule of thumbs for doing refactoring is to follow the SOLID principles.
What can we refactor? One thing that we could do is to introduce better names
for our packages. The main
package name does not say much about what the
inner components are so let’s use the name deck
instead. By changing the name
of the package we are following SRP by grouping components that change for the
same reason.
package deck
var (
ranks = []string{"ace"}
suits = []string{"hearts", "diamonds", "spades", "clubs"}
)
type Card struct {
Rank string
Suit string
}
type Deck struct {
Cards []Card
}
func NewDeck() Deck {
cards := []Card{}
for _, rank := range ranks {
for _, suit := range suits {
cards = append(cards, Card{Rank: rank, Suit: suit})
}
}
return Deck{Cards: cards}
}
Good we rename the package name and the test keep passing. Now there’s
something else that is showing up and is the fact that we have a name overload
for our constructor we don’t need to say that we are creating a new deck if we
are already using the deck
as a package name so let’s rename that
constructor:
package deck
var (
ranks = []string{"ace"}
suits = []string{"hearts", "diamonds", "spades", "clubs"}
)
type Card struct {
Rank string
Suit string
}
type Deck struct {
Cards []Card
}
func New() Deck {
cards := []Card{}
for _, rank := range ranks {
for _, suit := range suits {
cards = append(cards, Card{Rank: rank, Suit: suit})
}
}
return Deck{Cards: cards}
}
That looks accurate however what about the Card
struct? Do we really need to
export it? Are we going to use a Card
without a Deck
ever? Well to me it
doesn’t make sense to use just one a card without the entire deck unless you
are the Joker. So let’s not export this structure to the outside world.
package deck
var (
ranks = []string{"ace"}
suits = []string{"hearts", "diamonds", "spades", "clubs"}
)
type card struct {
Rank string
Suit string
}
type Deck struct {
Cards []card
}
func New() Deck {
cards := []card{}
for _, rank := range ranks {
for _, suit := range suits {
cards = append(cards, card{Rank: rank, Suit: suit})
}
}
return Deck{Cards: cards}
}
After doing that change and running the test we get a new compilation error:
deck_test.go|14| undefined: deck.Card
Here comes the confirmation of the use of _test
since we are not allowed to
use private members of our package we will need to think in terms of how the
client will use the information that belong to our two data structures. Instead
of initializing an expected list of cards let’s define it in terms of it’s
formatted data or in other words what a final user will want to know something that
we can build without thinking about too many internal details:
func TestNewDeck(t *testing.T) {
t.Run("when creating a new deck", func(t *testing.T) {
d := deck.New()
t.Run("it contains four aces of spades, clubs, diamonds and hearts", func(t *testing.T) {
aces := []string{}
expectedCards := []string{
`ace of hearts`,
`ace of diaminds`,
`ace of spades`,
`ace of clubs`,
}
for _, card := range d.Cards {
if card.Rank == "ace" {
aces = append(aces, card.String())
}
}
assert.Equal(t, expectedCards, aces)
})
})
}
After refactoring the test we get the following error:
deck_test.go|23| card.String undefined (type deck.card has no field or method String)
Because of course we are missing the implementation of the stringer
interface
let’s add that:
func (c card) String() string {
return fmt.Sprintf("%s of %s", c.Rank, c.Suit)
}
I think this is a good spot to stop; since the idea was to showcase just the Single Responsibility Principle.
Conclusion
As Sandi Metz said Design is the art of arranging code that needs to work
today, and to be easy to change forever
something that we can do with the TDD
cycle by making a failing test past and then moving the design towards the
SOLID principle through continuous refactor.