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:
type Order struct {
ID int
Customer Customer
Items []Item
Total float64
}
func (o *Order) AddItem(item Item) {
o.Items = append(o.Items, item)
o.Total += item.Price
}
func (o *Order) RemoveItem(item Item) {
for i, oItem := range o.Items {
if oItem.ID == item.ID {
o.Items = append(o.Items[:i], o.Items[i+1:]...)
o.Total -= oItem.Price
break
}
}
}
func (o *Order) Submit() error {
// Validate the order
if len(o.Items) == 0 {
return errors.New("Cannot submit an empty order")
}
if o.Total < 0 {
return errors.New("Invalid order total")
}
// Save the order to the database
// Send a notification to the customer
// Update inventory
// etc.
return nil
}
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:
type OrderValidator struct{}
func (v *OrderValidator) Validate(order Order) error {
if len(order.Items) == 0 {
return errors.New("Cannot submit an empty order")
}
if order.Total < 0 {
return errors.New("Invalid order total")
}
return nil
}
type OrderPersistence struct{}
func (p *OrderPersistence) Save(order Order) error {
// Save the order to the database
return nil
}
type NotificationService struct{}
func (n *NotificationService) SendNotification(customer Customer, message string) error {
// Send a notification to the customer
return nil
}
To use these structs in the Order struct
, you could pass them as dependencies to the Order constructor, like this:
type Order struct {
ID int
Customer Customer
Items []Item
Total float64
validator OrderValidator
persistence OrderPersistence
notification NotificationService
}
func NewOrder(customer Customer, validator OrderValidator, persistence OrderPersistence, notification NotificationService) *Order {
return &Order{
Customer: customer,
validator: validator,
persistence: persistence,
notification: notification,
}
}
Then, in the Submit method, you can use these dependencies to perform the tasks related to completing the order:
func (o *Order) Submit() error {
if err := o.validator.Validate(o); err != nil {
return err
}
if err := o.persistence.Save(o); err != nil {
return err
}
if err := o.notification.SendNotification(o.Customer, "Your order has been received and is being processed."); err != nil {
return err
}
return nil
}
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.