SOLID is an acronym that represents five pillars of object-oriented programming and code design theorized by Uncle Bob (A.K.A Robert C. Martin) around the 2000s. All developers should have a clear understanding of these pillars for developing software properly to avoid bad design.
Why Does SOLID Matter?
Caring about your code and how other engineers (or your future self) will read it and see its quality is undoubtedly the mission of any engineer who really cares about their product and their colleagues. Use SOLID best practices to reduce code complexity, reduce the coupling between classes, increase the separation of responsibilities, and delimit the relationships between them. These are simple ways to improve code quality.
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. — Martin Fowlero
A good design reveals intent, provides insights into requirements analysis, is adaptable, tolerates the evolution of business needs, and also accepts new technologies without undue cost.
But an understandable code doesn’t just happen! You have to craft it, you have to clean it, and you have to make a professional commitment.
Professional commitment?
Make it your responsibility to craft code that: delivers business value, is clean, is tested, and is simple.
I’ll try to explain SOLID principles in the simplest way so that it’s easy for everyone to utilize.
S — Single Responsibility Principle (SRP)
A class should have one and only one reason to change
In other words, one class should serve only one purpose. This does not presume that each class should have only one method, but they should all relate directly to the responsibility of the class.
Let's look at this Employee class as an example:
public class Employee {
private Integer id;
private String name;
private Double salary;
public Double calculateSalary() {
return this.salary - (this.salary * 0.225);
}
public void save() throws SQLException{
//Saves to database
}
public void printReport() {
// code to pint
}
}
How many reasons does it have to change?
- calculateSalary() belongs to the CFO team;
- save() belongs to the CTO team;
- printReport() belongs to the COO team.
Let's assume Employee A wants to change the rule to the business rule of calculateSalary and Employee B wants to change the format of printReport - this will cause a merge conflict. When responsibilities are mixed in the same source file, collisions become more frequent.
Then Employee B adds a new Report. The report looks pretty good. Business users are happy. However, during the next payroll cycle, all paychecks have the wrong amount. When responsibilities are co-located, accidental breakage becomes more likely.
HOW TO IDENTIFY VIOLATIONS OF SRP
Classes that violate SRP are:
- Difficult to understand;
- Change for too many reasons;
- Are subject to frequent CM collisions and merges;
- Cause a transitive impact and are subject to deployment thrashing.
O — Open Close Principle (OCP)
Entities should be open for extension, but closed for modification.
In other words, we are able to change or extend the behavior of a system without changing any existing code.
When we must change a module, we must also re-test, re-release, and re-deploy that module, and when we change old, working code, we often break it.
Software entities (classes, modules, functions, etc.) should be extendable without actually changing the contents of the class we're extending. If we follow this principle closely enough, it is possible to modify the behavior of our code without ever touching a piece of the original code. A change should be low cost and low risk.
Given the class:
package main.java;
public class Charge {
ClientGateway clientGateway;
public void run(Date chargeDate) {
for (Client client : clientGateway.findAll()) {
boolean chargeToday = false;
switch (client.getPaymentSchedule()) {
case BIWEEKLY:
chargeToday = DateUtils.isOddFriday(chargeDate);
break;
case MONTHLY:
chargeToday = DateUtils.isLastDayOfMonth(chargeDate);
break;
case WEEKLY:
chargeToday = DateUtils.isFriday(chargeDate);
break;
}
if (chargeToday) {
Money value;
switch (client.getPlan()) {
case MONTHLY:
value = client.getPlanValue();
break;
case DAILY:
List<CheckIn> frequencies = client.getCheckIns();
value = chargeCalculator.calcDaily(chargeDate, frequencies, client.getPlanValue());
break;
}
}
}
}
}
This code is messy and is obscuring its content. A good module should read like a story. But if we're not careful, that story can be obscured by little decisions, odd flags, or strange function calls. Let's figure out what went wrong here.
HOW TO IDENTIFY VIOLATIONS OF OCP: SWITCHING ON TYPE
- Rigidity: switch statements are repeated across many clients of Employee;
- Fragility: each will be slightly different and those differences must be located and understood.
- Immobility: Every module that has a type case depends on all the cases.
Let's look at a solution to these issues:
public class Charge {
ClientGateway clientGateway;
public void run(Date chargeDate) {
for (Client client : clientGateway.findAll()) {
if (client.isChargeDay(chargeDate)) {
Money value = client.calculateCharge(chargeDate);
...
}
}
}
}
This code tells its own story. It doesn’t need comments, and there are few obscuring details. The Client object has hidden most details behind an abstract interface, and this is what Object Oriented Design is all about. The revised code is:
- Not Rigid: new ChargeClassifications can be added to the Client class without modifying any existing code;
- Not Fragile: we don’t have to search through the code looking for switch statements;
- Mobile: Charge can be deployed with as many or as few Charge-Classifications as desired.
OCP is not an excuse for doing more than is necessary. Abstraction is expensive, and you should only use it when there is an imminent risk involved. In general, it's smart to prove to yourself that it will be worth the cost.
By creating appropriate abstractions, we can produce modules that are open for extension but closed for modification.
L — The Liskov Substitution Principle (LSP)
This principle states that a derived class must be substitutable for its base class.
Inheritance is a relationship between a base class and its derived classes, each derived class, and all clients of its base class.
Here is the original formulation: "If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T" (Liskov substitution principle).
In other words, instances of a derived class must be usable through the interface of its base class without clients of the base class being able to tell the difference. Subtypes must be substitutable for their base types.
LSP can also be described as a counter-example of Duck Test: “If it looks like a duck, quacks like a duck, but needs batteries — you probably have the wrong abstraction.” Let's look at how to recognize LSP violations.
HOW TO IDENTIFY VIOLATIONS OF LSP
- Abstract methods do not apply to all implementations;
- Lower level details become visible in high level policy;
- Base classes need to know about their subclasses;
- Clients of base classes need to know about the subclasses;
- Details creep up the hierarchy.
In the example above, there are no apparent OCP or LSP violations.
Substitutability in software is about how something behaves, not about what.
This principle encompasses the following points:
- LSP is a prime enabler of OCP;
- Substitutability supports extensibility without modification;
- Polymorphism demands substitutability;
- Derived classes must uphold the contract between their base class and its clients!
LSP is all about how it behaves-as-a and not how it is-a. Inheritance != IS-A
I — The Interface Segregation Principle (ISP)
Clients should depend only on methods they call.
A client should never be forced to implement an interface that it does not use; clients shouldn’t be forced to depend on methods they do not use.
Look at the fat interfaces below:
In the example above, methods are grouped for ease of implementation, and there are more methods than any one client needs. The problem is Phantom Dependency!
A change on behalf of one client impacts all clients, and a new client impacts all existing clients.
In general, an interface should be small, but sometimes a class with a fat interface is difficult to split. The good news is that you can fake it, as in the example below:
NOTES ON ISP
ClientGateway still has a fat interface, but clients don’t know this because all dependencies point AWAY from it, phantom dependency has been eliminated, and changing existing interfaces or adding new interfaces does not impact other interfaces. Therefore, other clients are not impacted.
D — The Dependency Inversion Principle
Low level details should depend on high level policy.
High level policy should be independent.
Entities must depend on abstractions, not on concretions. This principle states that the high level module must not depend on the low level module, but they should depend on abstractions. The example below is a poor implementation that does not follow the Dependency Inversion Principle:
Sales depends upon all derivatives of Client, but only because it creates them. It only calls methods in Client.
In procedural designs, source code dependencies flow with control. This makes it easy to add functions to existing data structures without changing the data structures.
In object-oriented design, design dependencies can be inverted so that source code dependencies flow against control.
SOLUTION: ABSTRACT FACTORY [GoF]
In the example below, dependencies are inverted, and Sales no longer depends on derivatives of Client. This is a better implementation:
Conclusion
SOLID might seem to be a handful at first, but with constant usage and adherence to its guidelines, it becomes a part of you and your code. Remember that these are principles, not rules. The principles should be applied on a case by case basis when it makes sense.
Are the SOLID principles applicable to Functional Programming?
Of course!
Functional programmers want to separate their code to avoid crosstalk between responsibilities and users. They want to minimize the number of modules affected by a change.
Pass on what you have learned. — Yoda, Master.
Author
Magnum Fonseca
Magnum Fonseca is a Software Engineer at Avenue Code and Thor's father in his free time. He loves to play videogames, walk in the park, and enjoy a good beer.