Solid Principles in Java
What are the SOLID principles, why do we need them, and how to approach them correctly?
In software engineering, if you want to have understandable, flexible, and maintainable object-oriented design SOLID principles is the direction to look at. SOLID is an acronym for five design principles which are actually a subset of many principles promoted by American software engineer and instructor Robert C. Martin (also known as Uncle Bob). They were first introduced in his paper Design Principles and Design Patterns in 2000. But the actual acronym was introduced later, in 2004, by Michael Feathers.
Let’s look at each principle one by one. Following the SOLID acronym, they are:
- The Single Responsibility Principle
- The Open-Closed Principle
- The Liskov Substitution Principle
- The Interface Segregation Principle
- The Dependency Inversion Principle
The Single Responsibility Principle
This principle states that “a class should have only one reason to change” which means every class should have a single responsibility or single job or single purpose.
For instance, if we have a data model class, like let’s say Athlete, with the list of fields related to that entity, the class should change only when we change the entity.
Following Single Responsibility Principle benefits us in:
- Testing – less job for a class – less test cases;
- Merge conflicts – less job for a class – less reasons to change a class – less merge conflicts
- Lower coupling – less job for a class – less dependencies
And just in general, it’s easier to deal with small, well-organized classes, where it’s pretty obvious what job the class does.
But let’s have a look what happens when we omit Single Responsibility Principle. Let’s imagine that we have an app for GYM goers where a user can fill in his body parameters, like height and weight. They also can choose exercises, save their progress of adding weights in exercises, and also they can track their diet.
Imagine there is a class AthleteService which handles athlete’s progress in the GYM, keeps track of the diet, and creates the progress graph.
In this case our class has several reasons to be changed whether our athlete is working out in the GYM and adding some weights to the barbell or he/she is calculating the calories of their dinner, or they want to see the progress over period in the graph. We always go to the same class which is not a very good approach. But not only we are mixing up our gym and diet stuff in one class we are also mixing up our business and persistence logic which also violates Single Responsibility Principle.
To achieve the goal of the single responsibility principle, we should implement separate classes that perform a single functionality only.
For instance, for the GYM related stuff we will have BarbellWeightService…
and separately BarbellWeightPersistence.
And the same goes for the diet – NetCaloriesService…
and separately NetCaloriesPersistence.
And, of course, the separate class for creating the graphs – GraphService.
Now, as you can see, each class has its separate job to do which exactly what the first SOLID principle states.
The Open-Closed Principle
This principle states that “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification” which means you should be able to extend a class behavior, without modifying it.
To understand this principle let’s take a look at our last class which was the GraphService. I deliberately wrote it in a way that violates The Open-Closed Principle. The class has a method which creates a graph depending of its type – weights or calories in our example. But imagine that we want to add to our app a graph that shows changes in body weight over period of time. In this case we would have to change our GraphService.
To avoid this we should redesign a bit our GraphService. Instead of putting in the createProgressGraph method some business logic we should make an abstraction.
And now the GraphService instead of being a class becomes an interface.
And we will have separate classes for creating the graphs of different types which will implement the GraphSrvice.
And now when we want to add the body weight graph we simply add one more implementation of GraphService – BodyWeightGraph.
The Liskov Substitution Principle
This principle states that “Derived or child classes must be substitutable for their base or parent classes”. In other words, if class A is a subtype of class B, then we should be able to replace B with A without interrupting the behavior of the program.
This principle is a bit trickier than others, so let’s go directly to some code examples.
Here we have an abstract class SmartWatch and the list of methods which represent what a smart watch can do. And let’s imagine that we have two smartwatches – Apple Watch and Garmin. Let’s start with an Apple Watch.
An Apple Watch can do all the things that are represented by the methods in the abstract SmartWatch class, so we can say that both classes can replace each other without any issue for our program.
And now let’s have a look at a Garmin Watch. A Garmin Watch can receive a notification, but can’t send a notification (at least mine can’t). So having this type of a program design violates The Liskov Substitution Principle. What is the solution here? The solution lies in having instead of one abstract class with a list of all possible features several interfaces with separate features.
And now our Apple Watch can simply implement all the interfaces and our Garmin watch can take only those interfaces which have feature available for it.
The Interface Segregation Principle
This principle is fairly simple and it says that instead of having big interfaces with a list of different methods you should divide them into smaller ones, just with the specific behavior. In doing so we will avoid situations when we will be forced to implement features that we don’t need. Let’s get back to our GYM example and imagine having such interface.
In this case our GYM goer will be forced to do all these activities every time he enters the gym. Pretty sure he/she won’t be happy about this. To avoid this overtraining and violation of the Interface Segregation Principle we should split our GymGoer interface into four separate ones.
And now our athlete will be a lot happier to have the choice what to do in the gym and let’s say he/she decides that weight lifting and some cardio will be enough for today.
The Dependency Inversion Principle
The last SOLID principle is also a bit harder to understand at the beginning and it states:
- We should depend on abstractions (interfaces and abstract classes) instead of concrete implementations (classes).
- The abstractions should not depend on details; instead, the details should depend on abstractions.
To understand what all this means let’s again look at some code examples and let’s stick with our GYM app.
First start with an example that violates The Dependency Inversion Principle.
We have three basic GYM exercises:
And now we can construct our workout routine using these exercises.
This simple class will work and our workout routine will definitely will make us stronger, but what if after some time we want to change the exercises or add some new ones. In this case we will be forced to change our WorkoutRoutine class which will violate The Open-Closed Principle.
Also, the WorkoutRoutine class is a high-level module, and it depends on low-level modules such as SquatExercise, DeadLiftExercise, and BenchPressExercise. We are actually violating the first part of the Dependency Inversion Principle.
Also, by inspecting the doWorkout method of WorkoutRoutine class, we realize that the methods doSquat, doDeadLift, and doBenchPress are methods bound to the corresponding classes. Regarding the workout routine scope, those are details since they are types of exercises. Thus, the second part of the Dependency Inversion Principle is violated.
To fix these issues we need to add an abstraction, and in our case it will be an interface Exercise.
And now we will change our classes that represent exercises.
And the final refactoring is in our WorkoutRoutine class.
What we achieved by all those changes is that the WorkoutRoutine class does not depend on lower level modules, but rather abstractions. Also, low-level modules and their details depend on abstractions.
In this article we covered SOLID principles, we started with a bit of history and then one by one we went through all five principles with the code examples of cases that violate the principle and the solutions how to fix the issues. Hope you enjoyed the reading.