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.
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 anEmployee
struct clarifies that anEmployee
has all properties of aPerson
. - 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.
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.
Let’s explore a bad example to understand the misuse of embedding and interfaces in Go:
Go1package 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
embeddingEmployee
, which embedsPerson
. Person
having aWork()
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 intermediaryEmployee
struct may not be necessary.
Now let's refactor the previous example to follow best practices in Go:
Go1package 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 aWork()
method, making it more general.Employee
uses composition by having aPerson
field instead of embedding it. This simplifies the structure.Manager
maintains a logical structure with reduced complexity by defining its own behavior.
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.