golang and composition over inheritance
Recently, Andrew Cairns shared a post on Twitter/X featuring a shortened written version of his fantastic video, Composition over Inheritance Explained by Games. As I read through the article and watched the video again, I reflected on how, in Go, we don’t face the same concerns.
Go’s design philosophy emphasises simplicity, clarity, and efficiency, strongly favouring composition over inheritance. By leveraging composition, Go encourages the use of small, self-contained components that can be easily combined to build complex systems. This approach not only enhances code readability and maintainability but also provides greater flexibility and control over implementation details.
Additionally, Go’s interface system, which supports implicit implementation, further promotes the modular and reusable nature of compositional design, helping developers avoid common pitfalls associated with inheritance, such as the fragile base class problem.
Go’s approach to inheritance is fundamentally different from the traditional class-based inheritance found in languages like PHP, TypeScript, and Java. Instead of relying on class hierarchies, Go uses structs and interfaces to achieve behaviour composition. Behaviour is added to structs through methods, and interfaces are implemented implicitly, allowing for flexible and decoupled design.
While traditional object-oriented programming languages like PHP, TypeScript, and Java rely on explicit interfaces, class hierarchies, abstract classes, and mixins or traits to achieve polymorphism and reuse, Go uses implicit interfaces and struct embedding to compose behaviours and decouple the definition of an interface from its implementation. This approach avoids the complexities and rigidity of deep inheritance hierarchies, promoting better modularity, readability, and maintainability. Go’s straightforward and flexible interface system makes its design simpler and more efficient, fostering robust and adaptable codebases.
Composition vs Inheritance #
Composition and Inheritance are two fundamental concepts in object-oriented programming (OOP).
Composition involves building complex types by combining objects of other types. It promotes the creation of small, reusable components that can be assembled to provide more complex functionalities. This approach encourages flexibility and modularity, as components can be easily replaced or modified without affecting the entire system.
Inheritance, on the other hand, allows a class to inherit properties and methods from another class. This establishes a hierarchical relationship between the parent (superclass) and child (subclass), enabling code reuse and the extension of existing functionality. However, inheritance can lead to rigid structures and tight coupling, making systems harder to modify and maintain.
How Go Implements Composition and Inheritance #
Now that we have an understanding of composition and inheritance, let’s take a look at how Go implements composition through the use of structs and interfaces. Go allows developers to create flexible and modular code without relying on traditional inheritance hierarchies.
Structs and Embedding #
In Go, structs are the primary way to define complex data types. A struct is a sequence of named elements, called fields, each field has a name and a type. Field names can be specified explicitly or implicitly using embedding. Composition is achieved by embedding one struct within another either named explicitly or implicitly. This allows the embedded struct to become a part of the containing struct, effectively allowing the latter to inherit the fields and methods of the former.
type Base struct {
Num int
}
type Container struct {
Base // Embedded struct
Str string
}
In this example, we have two structs: Base
and Container
. The Base
struct is embedded into the Container
struct, allowing users of the Container
struct to access the Num
field found in the Base
struct. A working example can be found at
Go by Example: Struct Embedding.
Interfaces and Implicit Implementation #
Go uses an implicit interface implementation, which means that Go types must implement a set of method signatures defined in a Go interface in order to be considered to conform to that interface. Unlike other languages where you must explicitly declare that a type implements an interface, if a type has all the methods defined in an interface, it is considered to implement that interface automatically.
type Geometry interface {
Area() float64
Perimeter() float64
}
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
func (r Rectangle) Perimeter() float64 {
return 2*r.width + 2*r.height
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}
func Measure(g Geometry) {
fmt.Println(g)
fmt.Println(g.Area())
fmt.Println(g.Perimeter())
}
Here we have two structs: Circle
and Rectangle
. These both have Area
and Perimeter
methods, meaning both structs implement the Geometry
interface and can be passed into the Measure
method. A working example can be found at
Go By Example: Interfaces.
Conclusion #
Go’s design philosophy emphasises simplicity, clarity, and efficiency. This is clearly reflected in its preference for composition over inheritance. By encouraging the use of small, self-contained components, Go enables developers to build flexible and modular systems. This compositional approach not only enhances code readability and maintainability but also provides greater control over implementation details, resulting in more adaptable and robust codebases.
The simplicity that Go brings to this design is evident in the code produced with the language. Without the need to worry about class hierarchies and architectural issues like the fragile base class problem, developers can focus more on implementation.
The use of structs and embedding, combined with Go’s interface system that supports implicit implementation, avoids the pitfalls of deep inheritance hierarchies found in traditional object-oriented programming languages like PHP, TypeScript, and Java. This makes Go’s approach to software design both innovative and practical.
For those looking to delve deeper into these concepts, exploring the resources mentioned below can provide valuable insights. By embracing Go’s compositional model, developers can create more maintainable, scalable, and efficient software systems, aligning with the modern demands of software development.
Benefits of Go’s Composition and Inheritance Approach #
- Flexibility: By composing types using struct embedding and implicit interfaces, Go provides greater flexibility in how objects are constructed, interact, and are used.
- Modularity: Components can be developed, tested, and maintained independently, promoting cleaner and more modular code.
- Simplicity: Avoiding deep inheritance hierarchies keeps the codebase simpler and easier to understand.
- Reusability: Methods and functionalities can be reused across different types through embedding and interfaces without the constraints of traditional inheritance.
Further Reading and Resources #
For further research and resources, consider these sources:
- Patterns of Enterprise Application Architecture by Martin Fowler, which covers subjects such as Inheritance, Class Table Inheritance and more.
- Andrew Cairns fantastic Composition over Inheritance Explained by Games video.
- Composition over inheritance on Wikipedia
- The Go Programming Language by Alan A. A. Donovan and Brian W. Kernighan, which provides comprehensive coverage of Go’s design and idioms.
- Effective Go, an official document that offers guidelines and best practices for writing clear and idiomatic Go code.
- Go by Example: Struct Embedding
- Go By Example: Interfaces
- codecademy: Go Composition