How SOLID Principles Level Up Your Engineering?
SOLID is one of the most important thing you should look into code reviews
Hey everyone! As software engineers, we all know code review is a critical part of building robust, maintainable, and scalable systems. It's not just about finding bugs; it's about fostering shared understanding, spreading knowledge, and improving code quality.
And when it comes to quality, few concepts are as powerful as SOLID.
SOLID is an acronym for five fundamental principles of object-oriented design proposed by Robert C. Martin (Uncle Bob).
While they might seem theoretical, they are incredibly practical, especially when you're scrutinizing a pull request. Let's break down each one and see how they empower effective code reviews.
S: Single Responsibility Principle (SRP) - "One Reason to Change"
What it means: A class or module should have only one reason to change. In other words, it should have one, and only one, job.
In Code Review: This is your first red flag. When reviewing a class, ask yourself:
"Does this class do more than one thing?"
"If requirements change for feature A, will this class also need to change because of feature B?"
If the answer is yes, it's likely violating SRP. A class that manages database interactions, sends emails, and handles business logic is a prime candidate for refactoring into smaller, focused units. This makes code easier to test, understand, and maintain.
Now, let’s see an example, I am omitting code because that will make it very long but I am talking about the code you may encounter.
Problematic Code (Violation):
Imagine aUserHandler
class that:Validates user input.
Saves user data to a database.
Sends a welcome email to the user.
If the email template changes, or the database schema updates, or validation rules shift, this single class has multiple reasons to change. It's doing too much!
Better Code (Adheres to SRP):
Separate these concerns into distinct classes:UserInputValidator
UserRepository
(for database operations)EmailService
(for sending emails)UserRegistrationService
(orchestrates these to register a user). Now, each class changes only for one specific reason. ✅
O: Open/Closed Principle (OCP) - "Open for Extension, Closed for Modification"
What it means: Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. You should be able to add new functionality without altering existing, working code.
In Code Review: Look for situations where a new feature requires changes to existing, stable classes.
"If we add a new payment gateway, do we have to modify the existing
PaymentProcessor
class, or can we just add a new implementation?""Is this code designed to handle future variations gracefully without being touched?"
OCP often points towards good use of interfaces, abstract classes, and polymorphism, allowing you to "plug in" new behaviors.
Now, let’s see an example:
Problematic Code (Violation):
APaymentProcessor
class with a largeif-else
orswitch
statement to handle different payment methods (e.g.,processCreditCardPayment()
,processPayPalPayment()
). Adding a new payment method (like Bitcoin) forces you to modify this existing class.Better Code (Adheres to OCP):
Define anIPaymentMethod
interface. Each payment method (e.g.,CreditCardPayment
,PayPalPayment
,BitcoinPayment
) implements this interface.
ThePaymentProcessor
then operates on theIPaymentMethod
abstraction. You can extend the system by adding newIPaymentMethod
implementations without modifying thePaymentProcessor
. ✅
L: Liskov Substitution Principle (LSP) - "Subtypes Must Be Substitutable for Their Base Types"
What it means: If S
is a subtype of T
, then objects of type T
may be replaced with objects of type S
without altering any of the desirable properties of T
. Essentially, a subclass should not break the functionality or contract of its parent class.
In Code Review: This principle helps you evaluate inheritance hierarchies.
"If I use this subclass where its base class is expected, does it behave as I'd expect the base class to behave, or does it introduce unexpected side effects?"
"Does this subclass enforce stronger preconditions or weaker postconditions than its superclass?"
Violations often lead to unexpected bugs or tricky conditional logic to handle specific subclass behaviors.
Now, let’s see an example:
Problematic Code (Violation):
You have a Bird
class with a fly()
method. Then you create an Ostrich
class that inherits from Bird
but its fly()
method throws an UnsupportedOperationException
.
A client expecting a Bird
and calling bird.fly()
would break when given an Ostrich
—the Ostrich
isn't substitutable for Bird
without altering the program's correctness.
Better Code (Adheres to LSP): If not all birds fly, you might redefine
Bird
as a more general concept, or introduce anIFlyable
interface.Bird
(abstract base, perhaps withoutfly()
)FlyingBird
(implementsIFlyable
)Ostrich
(inherits fromBird
, but doesn't implementIFlyable
). This ensures that anyBird
you treat asIFlyable
will actually be able to fly. ✅
I: Interface Segregation Principle (ISP) - "Clients Should Not Be Forced to Depend on Interfaces They Do Not Use"
What it means: Rather than one large, monolithic interface, it's better to have many small, client-specific interfaces. Clients should only be exposed to the methods they need.
In Code Review: Check for "fat" interfaces.
"Does this interface contain methods that not all of its implementing classes will genuinely use?"
"Is a client of this interface forced to implement methods irrelevant to its purpose?"
Splitting large interfaces into smaller, more granular ones prevents clients from being coupled to functionality they don't care about, making the system more flexible and easier to change.
Here is an example of code you may encounter during Code review that violates this principle and how you can fix it
Problematic Code (Violation):
A singleWorker
interface with methods likedoWork()
,eatLunch()
,takeVacation()
, andrechargeBattery()
.
If you have aHumanWorker
and aRobotWorker
Implementing this:RobotWorker
might throw an error foreatLunch()
ortakeVacation()
.HumanWorker
might throw an error forrechargeBattery()
. They are forced to implement methods irrelevant to their nature.
Better Code (Adheres to ISP):
Break it into smaller, specific interfaces:IWorkable
(withdoWork()
)IEatable
(witheatLunch()
)IRestCapable
(withtakeVacation()
)IRechargeable
(withrechargeBattery()
) Now,HumanWorker
implementsIWorkable
,IEatable
,IRestCapable
, andRobotWorker
implementsIWorkable
,IRechargeable
. Clients only see the methods they need. ✅
D: Dependency Inversion Principle (DIP) - "Depend on Abstractions, Not Concretions"
What it means:
High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
In Code Review: This is about how modules interact.
"Is this high-level business logic directly calling concrete database classes, or is it interacting with an interface that can be easily swapped?"
"Are we passing concrete implementations around, or are we relying on dependency injection to provide abstractions?"
DIP promotes loose coupling, making your code more modular, testable, and easier to refactor.
Here is an example scenario where code violates DIP and how you can fix it
Problematic Code (Violation):
AReportGenerator
class directly creates and uses a concreteCSVDataSourc
e orDatabaseConnector
class within its methods.class ReportGenerator {
private CSVDataSource dataSource = new CSVDataSource();
// ...
}
The high-levelReportGenerator
depends on the low-level, concreteCSVDataSource
. This makes it hard to change the data source (e.g., to a database) without modifyingReportGenerator
.Better Code (Adheres to DIP):
ReportGenerator
depends on an abstraction (IDataSource
interface). The concreteCSVDataSource
orDatabaseDataSource
implementsIDataSource
.class ReportGenerator {
private IDataSource dataSource;
public ReportGenerator(IDataSource dataSource) {this.dataSource = dataSource;
} // ...
}
TheIDataSource
is "injected" (provided) to theReportGenerator
. Both now depend on theIDataSource
abstraction, making the system loosely coupled and highly testable.
Wrapping Up: SOLID in Action
Applying SOLID principles during code reviews transforms them from mere bug-hunting expeditions into opportunities for architectural improvement.
By consistently asking the right questions through the lens of SOLID, you're not just reviewing code; you're building a more resilient, adaptable, and understandable software system.
It helps teams speak a common language about design quality and makes future development a much smoother ride.
What's your favorite SOLID principle to enforce during code reviews? Share your thoughts below!
Sometimes, the logic stored into a class is shared by several departments (i. e. Sales and Accounting). The issue here is that both departments control that class; so, potentially you are violating the SRP principle too.
You should refactor this class so the logic business of it goes to splitted classes, each one for each department. This way, you don't violate this principle. The shared logic business should go to the Shared folder at your project.
Thoughts?