In the world of programming, you can often encounter developers who use various tools and design patterns without understanding their deeper essence. This leads to incorrect use of these tools and, as a result, to the creation of code that is difficult to maintain and develop.
Understanding fundamental principles and the reasons for their emergence allows making informed decisions about program architecture. When a developer deeply understands why a certain principle is important, they can better evaluate when it should be applied and when a simpler solution would suffice. This is especially important in the context of SOLID principles, which are often misinterpreted as dogmas rather than tools.
It's important to remember that any tool is a means of solving a specific problem. Blindly following principles without understanding their purpose can lead to excessive code complexity and the emergence of abstractions that provide no real benefit. Therefore, before applying any principle or pattern, it's worth clearly understanding what problem it solves and what advantages it provides in a specific case.
SOLID is an acronym for five object-oriented design principles that help create more maintainable, flexible, and scalable code.
Let's examine each principle separately with implementation examples in Golang.
The Single Responsibility Principle states that each class should have only one reason to change. In the context of Go, this means that each package, structure, or function should perform only one specific task.
Let's look at an example of incorrect implementation:
type User struct {
Name string
Email string
}
func (u *User) SaveToDatabase() error {
// Logic for saving user
return nil
func (u *User) SendEmail() error {
// Logic for sending email
return nil
}
As we can see from the example, the User
structure has two methods SaveToDatabase
and SendEmail
. However, this doesn't comply with the single responsibility principle, as in this case the structure is overloaded with methods that are not inherent to it - working with the database and sending Email letters.
An example of correct implementation would be the following code:
type User struct {
Name string
Email string
}
type UserRepository struct {
// Has DB connection
}
func (r *UserRepository) Save(user User) error {
// Logic for saving user
return nil
}
type EmailService struct {
// Has Email sending configuration
}
func (s *EmailService) SendEmail(user User) error {
// Logic for sending email
return nil
}
In this example, we've divided responsibilities between different structures. Now the User
structure is only responsible for storing user data, UserRepository
handles database operations, and EmailService
is responsible for sending emails. This approach makes the code more modular, easier to test and maintain.
Each component has a clearly defined responsibility, which adheres to the SRP principle. If we need to change the database logic or email sending system, we can do so without affecting other parts of the system.
The Open/Closed Principle (OCP) is one of the fundamental principles of object-oriented design. It states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. This means:
This principle helps maintain system stability during extension and minimizes the risk of new errors in already working code.