- Published on
Go S.O.L.I.D: The Single Responsibility Principle
- Authors
- Name
- Moch Lutfi
- @kaptenupi
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:
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:
_25type OrderValidator struct{}_25_25func (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_25type OrderPersistence struct{}_25_25func (p *OrderPersistence) Save(order Order) error {_25 // Save the order to the database_25 return nil_25}_25_25type NotificationService struct{}_25_25func (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:
_18type 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_18func 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:
_12func (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.