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:
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.
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.
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:
Find the GitHub repository here. Also note that the salaries described here have nothing to do with reality and are defined merely for educational purposes. This example can be written in any language that supports the object-oriented paradigm, but I'll be using JavaScript with the TypeScript superset.
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 Payroll.ts
.
The calculateSalary
method takes two parameters of type String (thanks to Matias Battistello for correcting this) : employee
and hours
. The class also imports three other classes: Developer
, TeamLeader
, and 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:
Payroll
class to add the new category. Before long, we'll have too many if/else constructions.TeamLeader
class changes the name of the calculateTeamLeaderSalary
method to 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.new
keyword, 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 strategy pattern is behavioral and built on three pillars:
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 Developer
, TeamLeader
, and Recruiter
classes:
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 Payroll
class?
Finally, here's how an index file could consume our Payroll
class:
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.
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.