Motivation
This is the first post in a series where I’ll explain each SOLID concept clearly and engagingly. Instead of repeating content widely available online, I intend to provide a personal dump of my experiences and understanding of these principles. Today, we'll begin with the Single Responsibility Principle (SRP).
Brief Summary
The concepts forming the SOLID acronym have been well-known for at least 20 years and frequently appear in technical interviews. More importantly, deeply understanding these principles significantly improves your code quality. The purpose of these posts is to definitively clarify each concept so you can apply it independently of the programming language you use.
Note: At the end of the post, I’ll include two repositories with practical examples implementing each concept in Elixir and Golang. However, the essential aspect is grasping the underlying principles so you can adapt them to your specific context.
SRP - Single Responsibility Principle
This term was coined by Robert Martin (Uncle Bob) in 2000, in the book Design Principles and Design Patterns. However, this classic programming challenge emerged during the 1980s as application complexity grew significantly. This increased complexity necessitated better code organization, promoting the adoption of Object-Oriented Programming (OOP), which already existed for around two decades by the 80s and 90s.
According to the book Clean Code, the SRP can be summarized as follows:
"A class or module should have one, and only one, reason to change." (Clean Code, p. 138)
But what does that mean in practice? Within a structure, be it a module, package, or class, any interaction that deviates from its original purpose probably violates SRP.
Consider these two examples:
- Scenario 01: Within a user structure, properties and methods should always directly reflect user actions and characteristics. For example:
type User struct {
Name string
Age int
Document string
// other properties
}
// In Go, this "(u *User)" indicates the function is a method of the User struct.
func (u *User) IsAdult() bool {
// logic here
}
func (u *User) ValidateDocument(document string) bool {
// logic here
}
However, if we add something like:
func (u *User) SaveUser() {
// logic here
}
We clearly violate SRP, as saving the user should not be the direct responsibility of the domain structure User
.
- Scenario 02: It's also possible to violate this principle in other application layers. Consider this Elixir example:
# Code violating SRP
defmodule User do
def create_user(attrs) do
user =
%User{attrs}
|> Repo.insert!()
Email.send_welcome(user)
Logger.info("User created: \#{user.id}")
user
end
end
# Code adhering to SRP
defmodule UserCreator do
def create(attrs) do
Repo.insert!(%UserStruct{attrs})
end
end
defmodule WelcomeNotifier do
def send_welcome_email(user) do
Email.send_welcome(user)
end
end
This Elixir example clearly illustrates how easy it is to mix responsibilities. The initial code combines user creation, email sending, and logging into one function, violating SRP. The corrected code clearly separates responsibilities.
With these examples, it becomes easier to understand how we should approach writing code. Always evaluate whether each component has only one reason to change—if there's more than one reason, SRP is likely being violated in that context.
If you have any doubts or disagree with anything, feel free to comment!
Repositories with practical examples:
I’ll write about the other principles soon and link them here once published. See you next time!