Reducing Type Hierarchies

Oct 9, 2016


Introduction

I see a lot of developers coming to Go from object oriented programing languages such as C# and Java. Because these developers have been trained to use type hierarchies, it makes sense for them to use this same pattern in Go. However, there are aspects of Go that don’t allow type hierarchies to provide the same level of functionality they do in other object oriented programming languages. Specifically, the concepts of base types and subtyping don’t exist in Go so type reuse requires a different way of thinking.

In this post I am going to show why type hierarchies are not always the best pattern to use in Go. I’ll explain why it is better to group concrete types together not by a common state but by a common behavior. I’ll show how to leverage interfaces to group and decouple concrete types, and lastly, I will provide some guidelines around declaring types.

Part I

Let’s start with a program I see way too often from those trying to learn Go. This program uses a traditional type hierarchy pattern that would be commonly seen in an object oriented program.

https://play.golang.org/p/ZNWmyoj55W

Listing 1:

01 package main
02
03 import "fmt"
04
05 // Animal contains all the base fields for animals.
06 type Animal struct {
07	Name     string
08	IsMammal bool
09 }
10
11 // Speak provides generic behavior for all animals and
12 // how they speak.
13 func (a Animal) Speak() {
14	fmt.Println("UGH!",
15		"My name is", a.Name,
16		", it is", a.IsMammal,
17		"I am a mammal")
18 }

In listing 1 we see the beginning of our traditional object oriented program. On line 06 we have the declaration of the concrete type Animal and it has two fields, Name and IsMammal. Then on line 13 we have a method named Speak that allows an Animal to talk. Since an Animal is a base type for all animals, the implementation of the Speak method is generic and can’t represent any given animal very well beyond this base state.

Listing 2:

20 // Dog contains everything an Animal is but specific
21 // attributes that only a Dog has.
22 type Dog struct {
23	Animal
24	PackFactor int
25 }
26
27 // Speak knows how to speak like a dog.
28 func (d Dog) Speak() {
29	fmt.Println("Woof!",
30		"My name is", d.Name,
31		", it is", d.IsMammal,
32		"I am a mammal with a pack factor of", d.PackFactor)
33 }

Listing 2 declares a new concrete type named Dog which embeds a value of type Animal and has a unique field named PackFactor. We see the use of composition to reuse the fields and methods of the Animal type. In this case, composition is providing some of the same benefits of inheritance, with respect to type reuse. The Dog type also implements its own version of the Speak method, which allows the Dog to bark like a dog. This method is overriding the implementation of the Animal type’s Speak method.

Listing 3:

35 // Cat contains everything an Animal is but specific
36 // attributes that only a Cat has.
37 type Cat struct {
38	Animal
39	ClimbFactor int
40 }
41
42 // Speak knows how to speak like a cat.
43 func (c Cat) Speak() {
44	fmt.Println("Meow!",
45		"My name is", c.Name,
46		", it is", c.IsMammal,
47		"I am a mammal with a climb factor of", c.ClimbFactor)
48 }

Next we have a third concrete type named Cat in listing 3 that also embeds a value of type Animal and has a field named ClimbFactor. We see the use of composition again for the same reasons, and Cat has a method named Speak that allows the Cat to meow like a cat. Again, this method is overriding the implementation of the Animal type’s Speak method.

Listing 4:

50 func main() {
51
52	// Create a Dog by initializing its Animal parts
53	// and then its specific Dog attributes.
54	d := Dog{
55		Animal: Animal{
56			Name:     "Fido",
57			IsMammal: true,
58		},
59		PackFactor: 5,
60	}
61
62	// Create a Cat by initializing its Animal parts
63	// and then its specific Cat attributes.
64	c := Cat{
65		Animal: Animal{
66			Name:     "Milo",
67			IsMammal: true,
68		},
69		ClimbFactor: 4,
70	}
71
72	// Have the Dog and Cat speak.
73	d.Speak()
74	c.Speak()
75 }

In listing 4 we have the main function where we put everything together. On line 54, we create a value of type Dog using a struct literal and initialize the embedded Animal value and the PackFactor field. On line 64, we create a value of type Cat using a struct literal and initialize the embedded Animal value and the ClimbFactor field. Then, finally, we call the Speak method against the Dog and Cat values on lines 73 and 74.

This works in Go, and you can see how the use of embedding types provides familiar type hierarchy functionality. However there are some flaws with doing this in Go, and one is that Go does not support the idea of subtyping. This means you can’t use the Animal type as a base type like you can in other object oriented languages.

What is important to understand is that, in Go, the Dog and Cat types can’t be used as a value of type Animal. What we have is an embedded value of type Animal inside a value of type Dog and Cat. You can’t pass a Dog or Cat to any function that accepts values of type Animal. This also means that there is no way to group a set of Cat and Dog values together in the same list via the Animal type.

Listing 5:

// Attempt to use Animal as a base type.
animals := []Animal{
    Dog{},
    Cat{},
}

: cannot use Dog literal (type Dog) as type Animal in array or slice literal
: cannot use Cat literal (type Cat) as type Animal in array or slice literal

Listing 5 shows what happens in Go when you try to use the Animal type as a base type in a traditional object oriented way. The compiler is very clear that the Dog and Cat types can’t be used as type Animal.

The Animal type and the use of type hierarchies in this case is not providing us any real value. I would argue it is leading us down a path of code that is not readable, simple or adaptable.

Part II

When coding in Go try to avoid these type hierarchies that promote the idea of common state and think about common behavior. We can group a set of Dog and Cat values if we think about the common behavior they exhibit. In this case there is a common behavior of Speak.

Let’s look at another implementation of this code that focuses on behavior.

https://play.golang.org/p/6aLyTOTIj_

Listing 6:

01 package main
02
03 import "fmt"
04
05 // Speaker provide a common behavior for all concrete types
06 // to follow if they want to be a part of this group. This
07 // is a contract for these concrete types to follow.
08 type Speaker interface {
09	Speak()
10 }

The new program starts in listing 6 and we have added a new type called Speaker on line 08. This is not a concrete type like the struct types we declared before. This is an interface type that declares a contract of behavior that will let us group and work with a set of different concrete types that implement the Speak method.

Listing 7:

12 // Dog contains everything a Dog needs.
13 type Dog struct {
14	Name       string
15	IsMammal   bool
16	PackFactor int
17 }
18
19 // Speak knows how to speak like a dog.
20 // This makes a Dog now part of a group of concrete
21 // types that know how to speak.
22 func (d Dog) Speak() {
23	fmt.Println("Woof!",
24		"My name is", d.Name,
25		", it is", d.IsMammal,
26		"I am a mammal with a pack factor of", d.PackFactor)
27 }
28
29 // Cat contains everything a Cat needs.
30 type Cat struct {
31	Name        string
32	IsMammal    bool
33	ClimbFactor int
34 }
35
36 // Speak knows how to speak like a cat.
37 // This makes a Cat now part of a group of concrete
38 // types that know how to speak.
39 func (c Cat) Speak() {
40	fmt.Println("Meow!",
41		"My name is", c.Name,
42		", it is", c.IsMammal,
43		"I am a mammal with a climb factor of", c.ClimbFactor)
44 }

In listing 7 we have the declaration of the concrete types Dog and Cat again. This code removes the Animal type and copies those common fields directly into Dog and Cat.

Why did we do that?

  • The Animal type was providing an abstraction layer of reusable state.
  • The program never needed to create or solely use a value of type Animal.
  • The implementation of the Speak method for the Animal type was a generalization.
  • The Speak method for the Animal type was never going to be called.

Here are some guidelines around declaring types:

  • Declare types that represent something new or unique.
  • Validate that a value of any type is created or used on its own.
  • Embed types to reuse existing behaviors you need to satisfy.
  • Question types that are an alias or abstraction for an existing type.
  • Question types whose sole purpose is to share common state.

Let’s look at the main function now.

Listing 8:

46 func main() {
47
48	// Create a list of Animals that know how to speak.
49	speakers := []Speaker{
50
51		// Create a Dog by initializing its Animal parts
52		// and then its specific Dog attributes.
53		Dog{
54			Name:       "Fido",
55			IsMammal:   true,
56			PackFactor: 5,
57		},
58
59		// Create a Cat by initializing its Animal parts
60		// and then its specific Cat attributes.
61		Cat{
62			Name:        "Milo",
63			IsMammal:    true,
64			ClimbFactor: 4,
65		},
66	}
67
68	// Have the Animals speak.
69	for _, spkr := range speakers {
70		spkr.Speak()
71	}
72 }

On line 49 in listing 8, we create a slice of Speaker interface values to group both Dog and Cat values together under their common behavior. We create a value of type Dog on line 53 and a value of type Cat on line 61. Finally on lines 69 - 71 we iterate over the slice of Speaker interface values and have the Dog and Cat speak.

Some final thoughts about the changes we made:

  • We didn’t need a base type or type hierarchies to group concrete type values together.
  • The Interface allowed us to create a slice of different concrete type values and work with these values through their common behavior.
  • We removed any type pollution by not declaring a type that was never solely used by the program.

Conclusion

There is a lot more to composition in Go but this is an initial understanding around the problems with using type hierarchies. There are always exceptions to every rule, but try to follow these guidelines until you know enough to understand the tradeoffs for making an exception.

To learn more about composition and other topics this post touches, check out these other blog posts:

Exported/Unexported Identifiers In Go
Methods Interfaces And Embedded Types
Object Oriented Mechanics In Go
Composition With Go

Thanks

Here are some friends from the community I would like to thank for taking the time to review the post and provide feedback.

Daniel Vaughan, Ted Young, Antonio Troina, Adam Straughan, Kaveh Shahbazian, Daniel Whitenack, Todd Rafferty


Ultimate Go Programming LiveLessons

Ultimate Go Programming LiveLessons provides an intensive, comprehensive, and idiomatic view of the Go programming language. This course focuses on both the specification and implementation of the language, including topics ranging from language syntax, design, and guidelines to concurrency, testing, and profiling. This class is perfect for anyone who wants a jump-start in learning Go or wants a more thorough understanding of the language and its internals.

Learn more

Go Training

We have taught Go to thousands of developers all around the world since 2014. There is no other company that has been doing it longer and our material has proven to help jump start developers 6 to 12 months ahead of their knowledge of Go. We know what knowledge developers need in order to be productive and efficient when writing software in Go.

Our Go, Web and Data Science classes are perfect for both experienced and beginning engineers. We start every class from the beginning and get very detailed about the internals, mechanics, specification, guidelines, best practices and design philosophies. We cover a lot about "if performance matters" with a focus on mechanical sympathy, data oriented design, decoupling and writing production software.

Learn More

To learn about Corporate training events, options and special pricing please contact:

William Kennedy
ArdanLabs (www.ardanlabs.com)
bill@ardanlabs.com