What’s the deal with the SOLID principles?(part 1)

Razvan Soare
10 min readNov 30, 2018

--

Even if you are a junior developer there’s a great chance that you’ve heard about the SOLID principles. They are mentioned all the time. In a job interview you might have heard the questions: “So how do you assess the quality of your code? How do you distinguish good code from bad code?”. The general answers are something like: “I try to keep files as small as possible and move parts of the code into other files when a file gets too big.”. Among the worst answers is: “I’ll know it when I see it.”. After these types of answers the interviewer usually asks: “Have you heard of the SOLID principles?”.

So what’s the deal with the SOLID principles and why they are mentioned so often? According to Wikipedia: “In object-oriented computer programming, the term SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable. It is not related to the GRASP software design principles. The principles are a subset of many principles promoted by Robert C. Martin. Though they apply to any object-oriented design, the SOLID principles can also form a core philosophy for methodologies such as agile development or adaptive software development. The theory of SOLID principles was introduced by Martin in his 2000 paper Design Principles and Design Patterns “.

If followed correctly, the SOLID principles will lead to a high quality code base that is maintainable and testable. If someone asks for extra features you can make changes with minimal effort (when compared with code that doesn’t adhere to SOLID). I think that it’s important to mention that these principles are focused on maintainability, testability and greater code quality with regards to the human mind not the computer that you are programming for. Programmers who work in domains where performance is key have a different approach to programming. Because we live in a world where human time is more expensive than machine time, most programmers don’t work in high performance demand domains and are usually encouraged to use an object oriented approach. This being the case, the SOLID principles can be tackled by most developers.

Starting with this article I’ll try to explain each of the principles in the order that they appear in the acronym. To keep this brief I’ll split the theme in three parts. This is the first part in which I approach the first two principles. So let’s start with the letter S.

S

The ‘S’ from SOLID stands for Single Responsibility Principle (SRP) and it is the easiest principle to understand and perhaps the most ignored. What it means is that each class should do one and only one thing. If a class fails to accomplish its purpose you can look at it and say: “you had one job…”.

As an example, let’s say that we need to fetch some JSON data from the network, parse it, save it in the local database. Depending on what platform we’re programming on, this can be accomplished in not too many lines of code. Because there’s not much code, we may be tempted to throw everything in a class and forget about it. But according to SRP that would be a bad approach. We can clearly distinguish 3 separate responsibilities: fetch JSON from the network, parse JSON, save the parsed result into the database. In this situation we should have 3 classes.

The first class should only handle networking. We give it the URL and receive the JSON or an error if things go wrong.

The second class should just parse a JSON that it receives and return the result in a relevant format.

The third one should receive data in a relevant format and save it in the local database.

So why bother? What advantages do we gain from separating our code like this? One of the advantages is testability. We can use a test URL for the network class and see that it behaves correctly both on a successful and on an error test case. To test the JSON module we can give it a mock JSON and see that it produces the correct data. The same testing principles apply to the database class (give it mock data and test the results on a mock database).

With these tests in place, if our program fails, we can run the tests and see where it fails. It could be that something changed on the server and the data we receive is bad. Or maybe the data is fine but we missed something in our JSON parsing module and we’re not able to correctly parse the data. Or perhaps we are trying to insert data into the database in a column that is currently inexistent. By having these tests, we don’t have to guess where the problem is. We see where said problem is and we work on fixing it.

Besides testability we also gain modularity. If the project requirements change and the server returns XML or a custom format instead of JSON, all we have to do is write a new module that handles data parsing and replace our JSON module with this new one. Or perhaps, for some strange reason, we need both and the network module will call the correct one depending on some rules. What if we also have some local JSON files that we need to parse and send the data to some other module? Well, we send the local JSON to our parsing module and grab the result to use it where we need it. We need to save the data locally in a flat format instead of a database? No problem we exchange the database module with a new one that does just that.

As you can see, this seemingly simple principle has a lot of advantages. By adhering to it we can already envision significant improvements in the maintainability of our code base.

O

The ‘O’ stands for the Open-Closed Principle (OCP). The saying goes that our classes should be open for extension but closed for modification. What does this mean? The way I understand it is that we should write our classes and modules to be plug-able. If we need extra functionality we shouldn’t have to modify the class, we should be able to plug in a different class that provides the extra functionality. In order to explain what I mean, I will use a classic calculator example. This calculator can only do 2 operations: addition and subtraction. The calculator class can look something like this (the code in this article is not written in a particular language):

class Calculator {
public float add(float a, float b) {
return a + b
}
public float subtract(float a, float b) {
return a — b
}
}

We would use this calculator like this:

Calculator calculator = new Calculator()float sum = calculator.add(10, 2)//the value of sum is 12
float diff = calculator.subtract(10, 2)//the value of diff is 8

Now let’s say that the customer wants us to add multiplication to our calculator’s capabilities. In order to add this extra functionality we would have to edit the calculator class and add the multiply method:

public float multiply(float a, float b) {
return a * b
}

If the requirements change again and we need division, sin, cos, pow and many other mathematical functions, we’d have to edit the class again and again and add all those things. According to the OCP that is not a good idea. It means that our class is open for modification. We need to make it closed for modification but open for extension. So how do we do that?

We will first define an interface called Operation with a single method called compute:

interface Operation {
float compute(float a, float b)
}

After that we can create operation classes by implementing the Operation interface (most of the examples presented in this article can also be accomplished by using inheritance and abstract classes, but I prefer using interfaces). We will write addition and subtraction classes in order to recreate the simple calculator example:

class Addition implements Operation {
public float compute(float a, float b) {
return a + b
}
}
class Subtraction implements Operation {
public float compute(float a, float b) {
return a — b
}
}

Our new Calculator class will have a single method called calculate in which we pass the numbers and the operation:

class Calculator {
public float calculate(float a, float b, Operation operation) {
return operation.compute(a, b)
}
}

We’ll use our new calculator like this:

Calculator calculator = new Calculator()Addition addition = new Addition()
Subtraction subtraction = new Subtraction()
float sum = calculator.calculate(10, 2, addition)//the value of sum is 12
float diff = calculator.calculate(10, 2, subtraction)//the value of diff is 8

Now if we need to add multiplication, we’ll create a multiplication operation like this:

class Multiplication implements Operation {
public float compute(float a, float b) {
return a * b
}
}

and use it in the above example by adding:

Multiplication multiplication = new Multiplication()
float prod = calculator.calculate(10, 2, multiplication)//the value of prod is 20

We can finally say that our Calculator class is closed for modification and open for extension. Looking at this simple example you might say that there is no big deal to add those extra methods to the original class and that the presumably better approach implies writing more code. I’m inclined to agree with you in this simple situation, but there are real-life complex situations in which writing code by adhering to OCP helps. Perhaps you need to add more that three methods to each new functionality. Perhaps those methods are very complex. By following OCP we can externalise the new functionality in different classes. This helps us and others to better understand our code mainly because we have to concentrate on smaller chunks instead of scrolling through endless files.

To better visualise this concept we can think of the Calculator class as part of a 3rd party library and that we don’t have access to the source code. The upside is that the good people who wrote it, adhered to OCP and made it open for extension. As a result we can extend its functionality with our own code and use it in our project with ease.

If that still sounds silly, think of it this way: you just finished writing a great piece of software for a client. It does everything the client wants. You wrote everything to the best of your abilities and the code quality is amazing. A few weeks go by and the client wants new features. In order to implement them you must dive into your beautiful code and modify various files. Chances are that the code quality will suffer, especially if the deadline is tight. If you have written tests for your code (and you should), your changes might break some of them and you’ll also have to alter those tests.

Contrast that with code written according to OCP. All you have to do in order to implement new functionality is to write new code. The old code remains the same. All your old tests still work. Because we don’t live in a perfect world, in some remote cases you might still make small changes to parts of the old code but negligible when compared with the non-OCP approach.

There are also big psychological advantages. One of them being that instead of butchering a code base that you’re proud of in order to accommodate new functionality, you just write new code. By writing new code for new functionality instead of altering old code, higher team morale is ensued. This leads to higher productivity, which leads to a less stressful work environment and better quality of life.

I hope that you can see the importance of this principle. The downside is that on a real project, mostly due to the lack of functioning magical crystal balls, it is hard to look in to the future and see how and where we should apply this principle. But knowing about OCP does help to spot possible use cases when the need arises. After the client wants to add multiplication and division to the Calculator we add them to the Calculator class. When he also asks for sin and cos, we might say to ourselves: “Wait a minute…”. After waiting that minute we start refactoring the code to accommodate OCP and save ourselves future trouble. Now when the client asks for tan, pow and other features, we got things covered.

You can read about the next two SOLID principles in part 2. If you liked this post you can find more on our website.

--

--

Razvan Soare

Cofounder @ Bit Treat Software where we build mobile apps & games.