SOLID is an acronym for code that follows five design principles so it's highly cohesive, easy to read, and easy to maintain. Robert C. Martin, better known as Uncle Bob, came up with the SOLID principles in his seminal paper Design Principles and Design Patterns. They go as follows:
- Single-responsibility principle: There should never be more than one reason for a class to change.
- Open-closed principle: Software entities should be open for extension, but closed for modification.
- Liskov substitution principle: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
- Interface segregation principle: Many client-specific interfaces are better than one general-purpose interface.
- Dependency inversion principle: Depend upon abstractions, not concretions.
I always found it hard to learn SOLID only through definitions and concepts. They were too vague and abstract for me to understand SOLID in a tangible way. Design patterns helped.
Understanding the Open-Closed Principle through the Strategy Pattern
I started to understand the open-closed principle of SOLID because of the strategy pattern. Described in the famous 1994 book Design Patterns: Elements of Reusable Object-Oriented Software, the strategy pattern is about writing code so you have the ability to swap out different algorithms for different situations.
A Payroll Application
The best way to demonstrate how the strategy pattern works is through payment methods and/or shipping methods. So let's build a payroll application. Imagine we have to code an algorithm that calculates the salary of a company's employees. The roles are as follows:
- Team Leader
The Code before the Strategy Pattern
At first, it might be tempting to develop the payroll app so it has a class responsible for the salary calculation. Let's do exactly that and call it
calculateSalary method takes two parameters of type String (thanks to Matias Battistello for correcting this) :
hours. The class also imports three other classes:
Recruiter. The code has several conditionals that check what kind of employee is passed by the class constructor, after which it applies the correct salary calculation.
Immediately, we have a few challenges:
- Every time we create a new work category, we need to change our
Payrollclass to add the new category. Before long, we'll have too many if/else constructions.
- Imagine the
TeamLeaderclass changes the name of the
calculateSalary. This would break our code. Similarly, if an external API instead of a class, and it goes through a change in a future release, our code would break too.
- Our code is tightly coupled. Whenever we instantiate a concrete entity with the
newkeyword, we create a strong coupling. This is not recommended for well-designed software.
Let's take a look at how our employee classes are defined:
The Code after the Strategy Pattern
The strategy pattern is behavioral and built on three pillars:
- Context: what will use/consume the strategy pattern.
- Strategy interface: the abstract entity responsible for the contract that must be implemented by the interested classes and injected into the context.
- Concrete strategy: the concrete entities that implement the strategy interface. In our example, the employee classes.
We use the strategy pattern whenever we need to group or encapsulate similar algorithms. In our example, every employee category is similar, but has its own salary calculation algorithm. The "consumer" class (
Payroll) doesn't undergo any changes, even if we create new categories.
Let's first solve our coupling issue by making sure that our concrete classes depend on abstract instead of concrete entities. So instead of the
Payroll class instantiating concrete classes, it will receive a class implementing an interface (our strategy interface).
Let's write that out in code. First, we create an interface defining a common contract between all the employee classes:
Notice that this interface contains only one method,
calculateSalary. All employee classes must implement this method, because they must all implement the
Employee interface. Let's see what that looks like in our
As you can see, all the employee classes implement the
IEmployee interface. Because of this, all classes must implement the same
calculateSalary method (i.e. they must follow the same contract). But what about our
Finally, here's how an index file could consume our
As you can see,
Payroll doesn't know anything about any employee class. It only receives the abstract
IEmployee type as a dependency injection. This means we can remove all
new keywords from our
Payroll class and have it totally decoupled!
No matter how many new employee categories we create, the class won't need to be changed. In other words, it is closed for modification but open for extension, which is exactly the open-closed design principle.
As a Bonus
You've not just learned about the letter O in the SOLID acronym. You've also learned about the letter D. The dependency inversion principle states that classes should depend on abstract over concrete entities. We achieved exactly that in our example, because we passed an abstract representation of our classes as a parameter in our class constructors.
Not only that, but you've also learned about the letter S. The single responsibility principle states that a class must have only one reason to change. Before we applied the strategy pattern, our code had many reasons to change. For example, if the
TeamLeader class would change the name of its salary calculation method, or if we would create a new employee category, our
Payroll class would have to change as well.
This violated the single responsibility principle. With the strategy pattern applied, the
Payroll class will only ever change if the defined interface changes. It has one and only one reason to change, which is exactly what the single responsibility principle demands.
The hardest part of programming isn't learning a new programming language or paradigm. Instead, it's about understanding how to identify a problem and what design pattern you can apply to solve that problem. Understanding how code violates the best practice design patterns is the mark of an experienced programmer. The strategy pattern can help you understand the S, O, and D of SOLID code.