Robert Martin, author of Clean Code, described 10 principles around object-oriented design. The first five are known as SOLID principles. SOLID is an acronym for:
- S – single-responsibility principle
- O – open-closed principle
- L – Liskov substitution principle
- I – interface segregation principle
- D – dependency inversion principle
These principles allow us to design software that is readable, testable, and easy for multiple people to work on. In this post, I will break each of these down into more detail.
Single-responsibility Principle
The single-responsibility principle (SRP) states that each class, module, or function should have one responsibility and only one reason to change.
When we have to frequently make changes to a class for different reasons, it is a sign we need to break that class up into smaller pieces. For example, let’s say we were developing a chess game. We would probably have a class like Board that was in charge of updating the board’s UI whenever a move was made. We wouldn’t want it to also be in charge of knowing all the different pieces and in which ways they can move. That should be separated into other classes.
When we separate concerns in this manner, it leads to better error handling. We can much more easily know which part of our application is causing the error. The SRP also improves the maintainability and extensibility of our codebase. Other developers can more easily understand the codebase and make changes without introducing bugs.
Open-closed Principle
The open-closed principle states:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
This means that we should be able to add new functionality without changing the existing code.
For example, take a look at this JavaScript module that gets the attendance of a group of students:
Every time we add or remove a student or their attendance status changes, our module will need to change. This violates the open-closed principle. However, if we modified our getAttendance() function to take a parameter of students, we could make changes to our student group without having to touch our module:
The open-closed principle is now satisfied.
Liskov Substitution Principle
The Liskov substitution principle states:
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
In other words, you should be able to substitute a subclass for its parent class without breaking the code. Let’s look at an example:
This is a simple example, but the benefit of this is increased code reusability and maintainability. If we ever decide we want to add other types of birds to our application, we can do so easily.
Interface Segregation Principle
The interface segregation principle (ISP) states that no code should be forced to depend on methods it does not use.
In some programming languages, interfaces allow us to provide a level abstraction that gives other developers a good idea of what the code is doing without having to know all the details. While JavaScript does not support interfaces, I feel like the ISP is still very applicable to the language.
First, let’s take a look at some C# code using interfaces:
As you can see, this interface is problematic because it contains methods for the most high-end coffee maker but a basic coffee maker only needs to brew coffee. Our KeurigCoffeeMaker class is forced to implement methods that it will never use. This violates the ISP.
As a JavaScript developer, you may be thinking that this is inapplicable because JavaScript doesn’t have interfaces. While there are no interfaces in JavaScript, a similar problem can arise when using class inheritance. Take a look at the following code:
Here is an example using a GameConsole base class along with two child classes inheriting from it. After creating this, it occurred to me that a game console would also have functionality like being able to be turned on or off, but let’s ignore that for simplicity.
This code will run and work fine. The issue is that both our XboxOneS and PS1 classes inherit methods that they can’t use. We have to explicitly return null in those methods which results in a messy solution. In large projects, this would only be more cumbersome. Every time we add another child class to our project, we would have to go back and make sure that all of the inapplicable code was handled so we wouldn’t get unexpected behavior. This defeats the purpose of inheritance, which is meant to make our code more reusable and maintainable.
So what’s the solution then? We can break apart our large GameConsole class into individual components:
The benefit of this is that we can add functionality to our class as we need it. We no longer have to inherit all of the methods from the base class at once, some of which may not be applicable. This results in much more extensible and maintainable code, and it satisfies the ISP.
Dependency Inversion Principle
Finally, the D in SOLID stands for the dependency inversion principle (DIP). It states:
High-level modules should not depend on low-level module; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.
As before, let’s take a look at a code example to better understand. Let’s say we had a GasStation class that implements Exxon:
This code works fine for now, but let’s say that later we want to implement our GasStation using Avia. Avia uses different units and their API is structured differently so that the car object is passed to the methods instead of the constructor. This would require us to make a lot of changes in our GasStation class:
We had to change our constructor and we had to change both of our methods to implement the Avia object. Also, we had to pass in multipliers to convert from gallons to liters and from PSI to kPa. It may seem like this wasn’t a lot to change. However, in a real world application, you can imagine how tedious this would be whenever we had to change from one API to another. We can remedy this by creating an abstraction, or a middle layer, that our GasStation class depends on:
Now, our GasStation class is much more robust. Whenever we decide we want to switch the car services we use, we can do so easily without having to affect our class. This satisfies the DIP.
Conclusion
At the job I started recently, SOLID principles were emphasized by the senior developers. And I already knew they must be important because many job listings mention them as well. And for good reason. The more you adhere to them, the more maintainable and extensible your codebase will be. You will be grateful you applied them as your application grows in size!