
Viva La Duck
Our next code example will illustrate several of the SOLID design principles applied to our Go implementation.
In our Viva La Duck application, our duck must visit a number of ponds looking for bugs to eat. To keep things simple, we'll assume that each stroke will require the duck to eat one bug. Each time the duck paddles its feet (one stroke), the duck's supply of strokes is decreased by one.
We're not concerned with how the duck moves from pond to pond, but rather the number of strokes the duck must make to traverse the length of the pond. If a pond has bugs to eat, they will be found on the other side of the pond. If the duck runs out of energy, it dies.
Our program is a self-contained runnable Go source file. Its package name is main and it has a main() function. We'll use the DASHES constant later when we print the statistics indicating what the duck encountered at each pond.
The Pond struct contains the state of each pond, that is, the number of bugs it supplies for the duck to eat and how many strokes are required to cross the pond:
package main
import (
"fmt"
"errors"
"log"
)
const DASHES = "----------------------"
type Pond struct {
BugSupply int
StrokesRequired int
}
One of the first things we should do is define our system's behaviors in the form of simple interfaces. We should think about how we can embed our interfaces into a larger set of interfaces as we compose our system's behavior patterns. It makes sense to categorize a thing by its abilities because a thing is defined by its actions.
Since this is a book about functional programming, now would be a good time to mention that a major benefit of using interfaces is that they allow us to group our application's functions in order to model real-life behaviors:
type StrokeBehavior interface {
PaddleFoot(strokeSupply *int)
}
type EatBehavior interface {
EatBug(strokeSupply *int)
}
Each interface (StrokeBehavior and EatBehavior) represents a fine-grained, well-defined behavior. Breaking apart our system into small parts will make our application more flexible and more easily composable:
type SurvivalBehaviors interface {
StrokeBehavior
EatBehavior
}
By declaring small, single purpose interfaces, we are now free to embed them in new, more feature-rich interfaces.
Grouping interfaces is a common pattern we can find in the Go standard library. For example, in the httputil package, we find the following:
type writeFlusher interface {
io.Writer
http.Flusher
}
Next, we define our duck. Our duck is stateless and has no fields:
type Duck struct{}
We define two methods for our duck. The receiver, Duck, must be defined in the same package as our method, Stroke. Since we are only using a main package, that's not a problem.
Modeling our system after the real world, we define a Foot struct and a PaddleFoot method for that foot. Each time our duck paddles its foot, we'll decrement our duck's strokeSupply type:
type Foot struct{}
func (Foot) PaddleFoot(strokeSupply *int) {
fmt.Println("- Foot, paddle!")
*strokeSupply--
}
Similarly, we define a Bill type and its EatBug method that increments our duck's strokeSupply type:
type Bill struct{}
func (Bill) EatBug(strokeSupply *int) {
*strokeSupply++
fmt.Println("- Bill, eat a bug!")
}
For every stroke, our duck will paddle its foot.
Our Stroke method will return an error if the duck runs out of energy and gets stuck in the middle of a pond:
func (Duck) Stroke(s StrokeBehavior, strokeSupply *int, p Pond) (err error) {
for i := 0; i < p.StrokesRequired; i++ {
if *strokeSupply < p.StrokesRequired - i {
err = errors.New("Our duck died!")
}
s.PaddleFoot(strokeSupply)
}
return err
}
Now, we define our duck's eating behavior. When our duck reaches the end of the pond, it gets to eat all the pond's bugs:
func (Duck) Eat(e EatBehavior, strokeSupply *int, p Pond) {
for i := 0; i < p.BugSupply; i++ {
e.EatBug(strokeSupply)
}
}
The SwimAndEat method's signature is slightly different than that of Eat and Stroke methods. Notice the differences?
All three methods have a Duck as their receiver, but the SwimAndEat method defines the variable d. That's because we need to reference the Stroke and Eat methods within the SwimAndEat method.
Also, they all take an interface as their first parameter, but SwimAndEat takes a composed set of interfaces, namely StrokeAndEatBehaviors, which it uses polymorphically for both Stroke and Eat:
func (d Duck) SwimAndEat(se SurvivalBehaviors, strokeSupply *int, ponds []Pond) {
for i := range ponds {
pond := &ponds[i]
err := d.Stroke(se, strokeSupply, *pond)
if err != nil {
log.Fatal(err) // the duck died!
}
d.Eat(se, strokeSupply, *pond)
}
}