Composition in Go

 

Composition in Go

In Go, composition is achieved by including structures (types) within other structures. This is done in a straightforward and effective way, where the included structures are considered nested fields. This allows one structure to inherit the fields and methods of the nested structure without the need for classic inheritance.


Composition

Before to continue let us talk about what Composition is?, We can understand the Composition as a mechanism that involves building an object using other objects as components. Instead of inheriting properties and behaviors, a class uses instances of other classes to achieve its functionality.

It establishes a “has a” relationship. For example, a “Car” class can have a composition of a “Engine” class and a “Wheel” class.

Advantages

  1. Promotes low coupling, as classes do not directly depend on the implementation of other classes.
  2. Offers greater flexibility by allowing the combination and customization of objects through composition.
  3. Facilitates code reuse by using components in multiple contexts.
  4. Allows for the modification of components without affecting the main class, simplifying updates.
  5. Helps manage “has a” relationships, common in real-world systems.
  6. Facilitates the creation of complex objects through component aggregation.
  7. Can improve performance by avoiding the overhead of inheritance.
  8. Provides a clear way to model independent functional modules.
  9. Eases resource management, such as memory release when an object is destroyed.
  10. Enables the implementation of design patterns like the Decorator and Strategy patterns.
  11. Favors interface-based design, making testing and component substitution easier.
  12. Allows the creation of dynamic relationships between objects at runtime.
  13. Does not introduce inheritance problems like “is-a” and strong coupling.
  14. Facilitates the integration of third-party libraries and components into an application.
  15. Promotes a more modular and easily understandable code structure.

Disadvantages

  1. May require more code to establish and manage composition relationships.
  2. Excessive component aggregation can complicate object configuration and creation.
  3. Component reuse often depends on well-defined interfaces’ availability.
  4. Does not directly inherit features of component classes, potentially requiring more delegation work.
  5. Can result in the creation of objects consisting of a large number of components, which can be complex to maintain.
  6. The lack of a clear class hierarchy can make it challenging to identify “is-a” relationships.
  7. Requires careful design to ensure that components are well encapsulated and not overly coupled.
  8. Component lifecycle management can be complex in large-scale applications.
  9. May require a more abstract and interface-based approach, which can be harder for some programmers to understand.
  10. Composition may not be suitable for all situations, and in some cases, inheritance may be more appropriate.
  11. May require a greater initial design investment to define appropriate composition relationships.
  12. Modifying shared components may require updates in multiple places.
  13. Does not provide a natural mechanism to implement certain design patterns, such as the Singleton pattern.
  14. Code complexity can increase if solid design principles are not followed.
  15. Unit testing complexity may rise, especially when external components are used.

Example of Composition in Go

Let’s assume we are designing a vehicle management system that includes cars and motorcycles. Instead of using inheritance, we will use composition and define interfaces for types to implement.

This example illustrates how Go promotes composition and the use of interfaces instead of inheritance, making it easier to create flexible and maintainable systems.

package main

import "fmt"

// Define an interface for vehicles
type Vehicle interface {
Start()
Stop()
}

// Define a base structure for all vehicles
type BaseVehicle struct {
Brand string
Engine string
}

func (v BaseVehicle) Start() {
fmt.Println("Starting the", v.Brand)
}

func (v BaseVehicle) Stop() {
fmt.Println("Stopping the", v.Brand)
}

// Define a structure for cars
type Car struct {
BaseVehicle
Model string
}

// Define a structure for motorcycles
type Motorcycle struct {
BaseVehicle
Type string
}

func main() {
// Create a car
car := Car{
BaseVehicle: BaseVehicle{Brand: "Toyota", Engine: "V8"},
Model: "Camry",
}

// Create a motorcycle
motorcycle := Motorcycle{
BaseVehicle: BaseVehicle{Brand: "Harley-Davidson", Engine: "V-Twin"},
Type: "Sportster",
}

// Use composition to call methods of the Vehicle interface
vehicles := []Vehicle{car, motorcycle}

for _, v := range vehicles {
v.Start()
v.Stop()
}
}

Explaining the code:

We define a Vehicle interface with two methods: Start and Stop.

  1. We create a BaseVehicle structure that contains common fields for all vehicles. Then, we implement the Start and Stop methods in this structure.
  2. We define Car and Motorcycle structures, which embed (BaseVehicle) and inherit fields and methods from BaseVehicle.
  3. In the main function, we create instances of Car and Motorcycle and add them to a list of vehicles of type Vehicle. Then, we use a loop to call the Start and Stop methods on each vehicle, making use of composition and interfaces.