Published on

Go S.O.L.I.D: The Single Responsibility Principle

Authors

SOLID is a set of five principles of object-oriented design that were introduced by Robert C. Martin. These principles can help developers create more maintainable, scalable, and flexible software applications. The SOLID principles are:

  • Single Responsibility Principle (SRP): This principle states that a class should have only one reason to change. In other words, a class should have a single responsibility and should not have multiple responsibilities that are unrelated to one another.

  • Open-Closed Principle (OCP): This principle states that a class should be open for extension but closed for modification. In other words, you should be able to add new functionality to a class without changing its existing code.

  • Liskov Substitution Principle (LSP): This principle states that if a class is a subtype of another class, then it should be able to be used in the same way as the parent class without any issues.

  • Interface Segregation Principle (ISP): This principle states that clients should not be forced to depend on interfaces they do not use. In other words, you should create smaller, more focused interfaces rather than having a large, generic interface that has a lot of methods that may not be used by all clients.

  • Dependency Inversion Principle (DIP): This principle states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. This helps to reduce the coupling between modules and makes it easier to change the implementation of a module without affecting the rest of the system.

In Go, these principles can be applied in a variety of ways. For example, you can use interfaces to implement the Open-Closed Principle and the Interface Segregation Principle. You can also use dependency injection to implement the Dependency Inversion Principle. By following these principles, you can design your Go code in a way that is more maintainable, scalable, and flexible.

S — Single Responsibility Principle (SRP)

This post will be focusing on the SRP. The Single Responsibility Principle (SRP) is a software design principle that states that a class or module should have only one reason to change. In other words, a class or module should have a single, well-defined responsibility and should not have multiple responsibilities that are unrelated to one another.

Here is an example implementation when designing a struct to manage customer orders in an online store:

order.go

_37
type Order struct {
_37
ID int
_37
Customer Customer
_37
Items []Item
_37
Total float64
_37
}
_37
_37
func (o *Order) AddItem(item Item) {
_37
o.Items = append(o.Items, item)
_37
o.Total += item.Price
_37
}
_37
_37
func (o *Order) RemoveItem(item Item) {
_37
for i, oItem := range o.Items {
_37
if oItem.ID == item.ID {
_37
o.Items = append(o.Items[:i], o.Items[i+1:]...)
_37
o.Total -= oItem.Price
_37
break
_37
}
_37
}
_37
}
_37
_37
func (o *Order) Submit() error {
_37
// Validate the order
_37
if len(o.Items) == 0 {
_37
return errors.New("Cannot submit an empty order")
_37
}
_37
if o.Total < 0 {
_37
return errors.New("Invalid order total")
_37
}
_37
_37
// Save the order to the database
_37
// Send a notification to the customer
_37
// Update inventory
_37
// etc.
_37
return nil
_37
}

In this example, the Order struct has three methods: AddItem, RemoveItem, and Submit. The AddItem and RemoveItem methods are responsible for adding and removing items from the order, respectively. The Submit method is responsible for submitting the order and performing various tasks related to completing the order, such as validating the order, saving it to the database, and sending a notification to the customer.

By separating the responsibilities of the Order struct into distinct methods, we can ensure that each method has a single, well-defined responsibility. This makes the code easier to understand and maintain, as each method only has a single purpose and can be changed or modified independently of the other methods.

One way to separate the responsibilities of the Order struct in the example I provided would be to create separate structs for different responsibilities. For example, you could create a separate OrderValidator struct that is responsible for validating orders, a separate OrderPersistence struct that is responsible for saving orders to the database, and a separate NotificationService struct that is responsible for sending notifications to customers.

Here is an example of how these separate the struct:


_25
type OrderValidator struct{}
_25
_25
func (v *OrderValidator) Validate(order Order) error {
_25
if len(order.Items) == 0 {
_25
return errors.New("Cannot submit an empty order")
_25
}
_25
if order.Total < 0 {
_25
return errors.New("Invalid order total")
_25
}
_25
return nil
_25
}
_25
_25
type OrderPersistence struct{}
_25
_25
func (p *OrderPersistence) Save(order Order) error {
_25
// Save the order to the database
_25
return nil
_25
}
_25
_25
type NotificationService struct{}
_25
_25
func (n *NotificationService) SendNotification(customer Customer, message string) error {
_25
// Send a notification to the customer
_25
return nil
_25
}

To use these structs in the Order struct, you could pass them as dependencies to the Order constructor, like this:


_18
type Order struct {
_18
ID int
_18
Customer Customer
_18
Items []Item
_18
Total float64
_18
validator OrderValidator
_18
persistence OrderPersistence
_18
notification NotificationService
_18
}
_18
_18
func NewOrder(customer Customer, validator OrderValidator, persistence OrderPersistence, notification NotificationService) *Order {
_18
return &Order{
_18
Customer: customer,
_18
validator: validator,
_18
persistence: persistence,
_18
notification: notification,
_18
}
_18
}

Then, in the Submit method, you can use these dependencies to perform the tasks related to completing the order:


_12
func (o *Order) Submit() error {
_12
if err := o.validator.Validate(o); err != nil {
_12
return err
_12
}
_12
if err := o.persistence.Save(o); err != nil {
_12
return err
_12
}
_12
if err := o.notification.SendNotification(o.Customer, "Your order has been received and is being processed."); err != nil {
_12
return err
_12
}
_12
return nil
_12
}

By separating the responsibilities of the Order struct into separate structs, you can more easily follow the Single Responsibility Principle and make the code easier to maintain and understand.

In a real-world application, the Order struct might have additional methods for performing other tasks related to managing orders, such as canceling an order or processing a refund. However, each of these methods should have a single, well-defined responsibility, in accordance with the Single Responsibility Principle.