Lesson 4
Using Composition and Interfaces in Go for Clean Code Practices
Introduction

Welcome to another lesson of the Clean Code in Go course! In this course, we have explored foundational concepts like the Single Responsibility Principle, Encapsulation, and Struct Initialization, all essential for writing clear, maintainable, and efficient code. In this lesson, we'll focus on using composition wisely. By understanding the role of composition, struct embedding, and interfaces, we'll learn how to apply these effectively to enhance code readability and organization while maintaining the principles of clean code.

The Role of Composition, Embedding, and Interfaces in Writing Clean Code

In Go, composition is a powerful tool that allows for code reuse and logical organization without relying on traditional inheritance. Instead of creating a subclass from an existing class, you can create more complex types by embedding structs and using interfaces to define behaviors.

  • Code Reuse and Flexibility: By embedding one struct within another, you can reuse code without duplicating it, making it easier to maintain and extend.
  • Improved Readability with Structs and Interfaces: Composition through struct embedding makes it evident what each type does. For example, embedding a Person struct in an Employee struct clarifies that an Employee has all properties of a Person.
  • Adherence to Previous Concepts: Similar to inheritance, composition in Go should align with the Single Responsibility Principle and Encapsulation. Each struct should serve a clear purpose and maintain data integrity.
Best Practices for Using Composition and Interfaces

To leverage composition and interfaces effectively, it's important to follow these best practices:

  • Favor Composition Over Inheritance: Always consider using composition and struct embedding before attempting to mimic classical inheritance patterns, as they promote loose coupling.
  • Utilize Interfaces for Behavior Definition: Define interfaces for behaviors that different structs should implement, ensuring a clear separation of concerns.
  • Avoid Unnecessary Embedding: Avoid overly complex struct embedding that could lead to confusion and harder code maintenance.

Pitfalls include forcing struct embedding to mimic "is-a" relationships or misusing interfaces without proper logical structuring.

Bad Example

Let’s explore a bad example to understand the misuse of embedding and interfaces in Go:

Go
1package main 2 3import "fmt" 4 5type Person struct { 6 Name string 7 Age int 8} 9 10func (p Person) Work() { 11 fmt.Println("Person working") 12} 13 14type Employee struct { 15 Person 16 EmployeeID string 17} 18 19type Manager struct { 20 Employee 21} 22 23func main() { 24 manager := Manager{Employee{Person{"Alice", 30}, "E123"}} 25 manager.Work() 26 fmt.Println(manager.Person.Name, "should also be managing") 27}

In this example:

  • The struct hierarchy is unnecessarily deep, with a Manager embedding Employee, which embeds Person.
  • Person having a Work() method might be inappropriate because not every person works, making the base struct less general.
  • The embedding might be forced where a Manager "is-a" Person, but the intermediary Employee struct may not be necessary.
Refactored Example

Now let's refactor the previous example to follow best practices in Go:

Go
1package main 2 3import "fmt" 4 5type Person struct { 6 Name string 7 Age int 8} 9 10type Employee struct { 11 Person Person 12 EmployeeID string 13} 14 15func (e Employee) FileTaxes() { 16 fmt.Println(e.Person.Name, "filing taxes") 17} 18 19type Manager struct { 20 Employee 21} 22 23func (m Manager) HoldMeeting() { 24 fmt.Println(m.Employee.Person.Name, "holding a meeting") 25} 26 27func main() { 28 person := Person{"Alice", 30} 29 employee := Employee{Person: person, EmployeeID: "E123"} 30 manager := Manager{Employee: employee} 31 manager.HoldMeeting() 32}

In the refactored example:

  • Person no longer has a Work() method, making it more general.
  • Employee uses composition by having a Person field instead of embedding it. This simplifies the structure.
  • Manager maintains a logical structure with reduced complexity by defining its own behavior.
Summary and Next Steps

In this lesson, we explored how to use composition, struct embedding, and interfaces wisely to support clean code practices in Go. By favoring composition and ensuring clear, stable type designs, you can create more maintainable and understandable code. We've focused on how these principles can be powerful tools when used correctly, aligning with concepts like SRP and encapsulation.

Next, you'll have the opportunity to apply and solidify these principles with practice exercises. Remember, clean code principles are fundamental, and we encourage you to keep practicing and applying them in your coding endeavors.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.