Benefits of SOLID Design Principles. What a beautiful S!
In the world of software engineering, writing code is the easiest part, while the hardest is maintaining clean code. One of the questions that I often asked myself during my initial days of software development is, am I doing this right or introducing tight coupling? Even though it seems like a simple question, trust me, it may be tougher than you think to get the correct answer. So, I started reading about design principles and tried to apply them in any code I write.
Design principles are a set of best-practice concepts that can be implemented and maintained to ensure clean code. These principles have been tried and tested by so many developers over time. Think of these principles as the doctor for your code that helps keep your application clean, safe, and secure.
SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. These five principles are guidelines that may not be mandatory but are highly recommended for better software development. The principles are listed below:
- Single Responsibility Principle
- Open/Close Principle
- Liskov Substitution principle
- Interface Segregation Principle
- Dependency Inversion Principle.
There are tons of documents on the internet explaining these principles. In this blog, I will walk readers through these principles one at a time and provide examples of real-world scenarios to understand better how these principles provide better and cleaner software development.
To start with, let’s deep dive into SRP today.
Single Responsibility Principle (SRP) is, in a way, very simple, but at the same time, slightly confusing. SRP states that there should never be more than one reason for a class to change. In other words, a class should have a very specific purpose for its existence. It should be focused on one particular functionality and should not care about any additional functionality that may or may not be interdependent with the core functionality of the class.
Let’s walk through an example to understand a bit more. Let’s take the most common use case: logging in. We are going to create login functionality for a website. To start, let’s figure out what should be considered for a user to log in.
- The user will provide a username and password.
- The application will validate username and password in some data source.
- If credentials are valid, it will log in and send a success message; if not, it will send a failure message.
While keeping these three points in mind, let’s write some code to implement the feature. For now and for simplicity, we will use a hardcoded map for storing username and password.
If we look into the above code, it seems alright, right? We have two usernames and passwords stored in the datastore method. A username and password are initialized in the main method, using the username to get the password from the datastore. If the password from the datastore matches the given password, we consider credentials valid and print a success message; else, we print a failure message. So far, so good!
Well, not so much… Even though this code will run and do the job, looking closely, you will notice that this class is doing so many things: taking input username and password, validating input credentials, managing datastore, and finally printing message(s). This class is responsible for many things, meaning this class will change for any change in behavior for any of these components. If you would like to store data in a database or a file instead of the map, you need to change this class. Maybe you would like to log message rather than doing a System.out.println(..), that too calls for a change in this class. If you want the input username to come from a json/yaml file or a REST endpoint- the class needs to change again. It is evident that this class doesn’t have a single responsibility and is prone to quick changes, so ultimately, this class violates SRP.
The idea of having a Login.class is to help users pass credentials to the application for login. So this class should only change if we change the way the credentials are passed to the class and in no other cases. Login should not worry about the validation logic in place, the way the message is displayed, and etc.
Now that we are clear with the class’s responsibility let’s first try to make the class modular; in other words, separate each concern into smaller methods.
If you look into the code, we have separated each concern into smaller methods, giving us a good picture of the class’s responsibilities. Now the next step is to look into the class and try to define it.
I can define the class to be responsible for login and validation and storing data and printing success and failure messages. As a rule of thumb, whenever the word AND comes while defining the purpose of a class it tells that it is violating SRP and each contributor of the AND statement has to be written in separate classes. Let’s now try to refactor this class into smaller classes, each having a single responsibility.
The first step would be to define packages corresponding to each responsibility. I have used the following packages:
- caller: This package will contain the main class that will be responsible for calling the login logic with user credentials.
- controller: This package will have the Class that talks with all other classes to perform the login operation.
- datastore: This package is responsible for holding classes that will manage stored data
- message: As the name suggests this package will have classes responsible for generating messages
- validator: The class with validation logic will reside here.
Below is the new code, each class having a single responsibility.
With these changes, each class now has a single responsibility, you want to change the calling mechanism from main method to REST api, you just need to change LoginCaller and that is it, no other class needs to be changed. You want to change the way data is stored, change the logic of LoginDatabase.datastore() method and you will be done. The same is true for Message and LoginController.
As each class is doing its part and then calling a method of another class, so any changes inside those methods is unknown to the caller classes as long as the method signature remains intact. So in case something changes inside a method of a class, the caller need not have to know and worry if the code will run or not. This is the biggest benefit of SRP.
With this let’s revisit the definition of SRP- SRP states that there should never be more than one reason for a class to change. Hope with the aforementioned explanation, this definition is now much more clearer.
Notes from the author:
This article attempts to encourage new developers to understand the concepts of design principles and help them decide how to implement SRP while writing a piece of code. I have tried to keep the code simple so that it is easier to understand, even for those who are new to Java. The code shown in this article can be found at Github.
Though the code in this article is written in Java, know that the design principle language is agnostic. Design principles are a set of specifications or best practices that is true for any programming language.