A Forest Full of Trees

Are you a Go developer who wants a solid reference for how interfaces and struct composition work? Look no further! Inside the code sample on GitHub are a few submodules you might find handy. Feel free to open up that repo, but we will be referencing code from it here directly.

Inter-forest-aces

An interface in Go is an abstract type. That is, it describes something without actually embodying the thing itself. Interfaces take this shape:

type someIFace interface {
    someMethod() string
    someOtherMethod() int
}

Notice that in someIFace, there are two methods. One returns a string, and another an int. This is helpful if you want a function that operates on types, but you want to adapt what those types are situationally. To put it into context, take the interface from package trees.

// ITree describes an abstract tree
type ITree interface {
	GetCategory() string
	GetGenus() string
	GetSpecies() string
	GetCommonName() string
	Spring() string
	Summer() string
	Fall() string
	Winter() string
}

The interface says nothing about any particular tree. It just defines how a concrete tree will describe itself. Some of these methods are implemented (that is to say, these methods exist) on the concrete type Tree.

// Tree is a concrete structure to build a Tree with.
type Tree struct {
	genus, species, commonName string
}

// GetGenus returns the genus of the receiving tree.
func (t Tree) GetGenus() string {
	return t.genus
}

// GetSpecies returns the species of the receiving tree.
func (t Tree) GetSpecies() string {
	return t.species
}

// GetCommonName returns the common name of the receiving tree.
func (t Tree) GetCommonName() string {
	return t.commonName
}

However, since Tree doesn’t implement every method of ITree, it does not satisfy the interface.

Growing some deeper roots

While a Tree implements some of the methods that ITree requires, the methods related to the seaons are left unsatisfied. This is where both packages deciduous and conifer come into play.

Planting a seed (or a struct)

If you come from another language, you might be familiar with inheritance. This is the idea that something (usually a class) can pass down properties to other things (often called their children). The thing receiving these properties is inheriting those from its parent. Go doesn’t strictly have this, but we have something close called embedding. When we embed a struct into another, all of the properties of the outer struct are passed down to the struct doing the embedding. Here, both Conifer and Deciduous embed the Trees struct.

package conifer

// Confier is type of tree. All fields are
// composed of the Tree type from the trees package.
type Conifer struct {
	trees.Tree
}
package deciduous

// Deciduous is type of tree. All fields are
// composed of the Tree type from the trees package.
type Deciduous struct {
	trees.Tree
}

Since the trees.Tree struct has methods like GetCommonName and properties like Genus and Species, these two structs do as well.

A Fork in the Road

Conifers and deciduous trees have different lifecyles through. And they experience the turning of the seasons in very different ways. So we would not want them to receive those methods from a common Tree ancestor. This is why we implement these methods separately for these two different categories of tree. For example:

package deciduous 

// Spring returns a string representing the
// action deciduous trees take during spring.
func (d Deciduous) Spring() string {
	return "Ahhh, new leaves and flowers!"
}
package conifer

// Spring returns a string representing the
// action conifers take during spring.
func (c Conifer) Spring() string {
	return "Growing some cones!"
}

Both of these methods reveal how both differing kinds of trees experience the same thing in their own special way.

Bring it around the campfire

As previously mentioned, the real power of an abstract type is to pass it to a function that operates on interfaces. Take a look at the trees.Lifecycle function.

package trees 

// Lifecycle iterates over the methods found in ITree to describe
// the argument Tree and it's life over one year.
func Lifecycle(t ITree) {
	fmt.Println("Hello! I am " + t.GetGenus() + " " + t.GetSpecies() + " (" + t.GetCommonName() + "), " + t.GetCategory() + " tree!")
	fmt.Println("Today I am going through my lifecycle. Come grow with me!")
	fmt.Println("It's Spring! " + t.Spring())
	fmt.Println("Summertime! " + t.Summer())
	fmt.Println("Autumn is here. " + t.Fall())
	fmt.Println("Brrrr winter! " + t.Winter())
}

Lifecycle does one thing - it progresses through a year of whatever ITree is passed into it. It does not care whether the ITree is deciduous, a conifer, or a new, third type! But rather than having deciduous.Lifecycle and conifer.Lifecycle, you can have this one place to make these ITrees grow. Now how neat is that?

The wilderness must be explored!

Have some fun with this. Create some new types of trees that implement the ITree interface. Or maybe rename ITree to IPlant and create some vascular and nonvascular plant types and let deciduous and conifer embed those structs as necessary. Test yourself too. Knowing now what you do about interfaces, do you know why trees.New() can’t be passed into the trees.Lifecycle() function, but both a conifer.New() and a deciduous.New() can?