Writen by Claudio Ramon Trejes Dornelles,
5 minutes of reading
SOLID: Understand the 5 principles of Object-Oriented Programming
Although the five principles contained in the SOLID acronym are relatively simple in their definitions, their application opportunities can often be difficult to spot, especially for beginner developers.
SOLID is an acronym that encompasses five principles of object-oriented programming created by Robert Cecil Martin and mentioned in the article The Principles of OOD. According to this article, these principles are more focused on software dependency management which, when disregarded, can easily lead to code that is hard to change, fragile and non-reusable. These and other principles are addressed in the book Agile Software Development – Principles, Patterns and Practices, all of them related to dependency management and which together, according to the author, form the foundation for the development of flexible, cohesive and reusable code.
The five principles that form the SOLID acronym are respectively:
+++ Single Responsibility Principle (SRP)
+++ Open Closed Principle (OCP)
+++ Liskov Substitution Principle (LSP)
+++ Interface Segregation Principle (ISP)
+++ Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
The single responsibility principle says that a class should have one and only one reason to change. In other words, a class must have only one responsibility, be responsible for only one action or task within an application.
Let’s say we will develop an application to manage appointments in a doctor’s office and we will need a connection to a database to save patient and appointment information. We then choose a database that will meet the demand and create a class named “Database”, which will manage the information in the database. It will contain information such as database access data apart from methods responsible for managing patients and appointments, such as, for instance basic CRUD (Create, Read, Update and Delete) functions. Although in the beginning, when the system is still small and of low complexity, this seems like a proper approach, the “Database” class has more than one reason to change. Whenever any information about the database connection is changed (like login data, for instance), this class will have to be changed, and also if new search methods are required to obtain information of patients that had the most appointment in a certain month, for instance. These two modification examples have not direct connection to one another, but would require change in the same class, “Database”. Therefore, any change or new implementations that may be required during the evolution of the application will entail changes in parts that were already developed and working. In the event of an application not yet submitted to an effective amount of automated tests, we are exposed to a great risk of changing an already implemented behavior without noticing the improper change. In other words, we end up with code that is not cohesive, fragile, with a high degree of coupling between different parts of the system, hard to be tested and hard to be reused.
In order to illustrate the application of this principle, we could have as an example a class named ” DatabaseConnectionManager”, developed to manage database connections, contemplating only methods and attributes necessary for the establishment of a successful connection. Therefore, let’s see some positive points of the approach:
- Any modification in our application not specifically related to database connections will not be applied to the class “DatabaseConnectionManager”, which makes it cohesive as it has only a single responsibility and a single reason to be changed.
- The class becomes interdependent among the other parts of the system, as it needs not know other points not related to its connection scope, thus reducing the coupling level between classes.
- This class can be used whenever it is necessary to establish a new database connection in any points of our application, thus making it easier to reuse code.
- When writing automated tests for this class, we must focus on testing behaviors related only to its scope, whether the class behaves correctly when establishing database connections and nothing more (the ease to write automated unitary test is a great indication that a class has only one responsibility).
The single responsibility principle is one of the simplest principles, yet one of the most difficult to implement, as in some cases the mixing of responsibilities in a class is not evident, but remaining attentive in the search for classes with unnecessary responsibilities will make the code evolve towards a point of greater cohesion and flexibility.
Open Closed Principle (OCP)
The open closed principle sustains that a module, in order to observe this principle, must be open for extension, but closed for modification. In other words, a module may be able to act differently as the application requirements change, but its source code must remain untouched. Initially it may seem impossible to do, but the answer is in the use of abstractions.
We may use as an example a class responsible for calculating the area of geometric figures, which initially may receive two geometric shapes (circle and square), calculating the area of these shapes by the following procedure:
- If the shape is a square, it computes the area by multiplying the base by its height.
- If the shape is a circle, it computes its area by multiplying a constant Pi by the square of the circle’s radius.
Now let’s say that our application has evolved and it became necessary to calculate the area of triangles. Here the calculation class must be modified so that a new verification is made, validating whether the geometric shape is a triangle and then making the new calculation. This would violate the open-closed principle, as we would be modifying the calculation class source code. In order to circumvent this situation we must use abstractions to “close” our code against changes, such as for instance make our calculation class receive as argument a geometric shape instead of circles or squares and then invoke a function ‘calculateArea()’ of each of the geometric shapes received. Thus circles, squares and triangles may extend/implement this generic shape and overwrite the method ‘calculateArea()’ implementing the calculation in its own manner. By using this new approach the calculation class code will not be changed whenever a new geometric shape is introduced in the application, only a new extension will be made in the generic geometric shape, thus observing the open-closed principle.
In spite of this example of how to implement this principle, we must bear in mind that an application cannot be “closed” entirely. We must always think of the application’s possibilities of change and evolution to determine what does and what does not make sense to keep closed to changes. As an example, think that a new business rule has been established for the previously used application: we must always calculate the area for circles first, then squares and finally triangles. The calculation class is not closed to a change like this and will never be entirely closed to a new change.
Liskov Substitution Principle (LSP)
This principle was first written by Barbara Liskov, establishing that functions receiving objects from a class must be capable of receiving objects derived from this same class with no need to know how to distinguish them.
If we think of the figure Rectangle we can remember our school days when we learned that every Square is a Rectangle, and then we create a class Square which inherits the behaviors of Rectangle. In this case, Rectangle may contain methods modifying its height and width while Square would have only one method modifying its sole property “side”. If our application has a method expecting to receive a Rectangle in its signature to then double its size, we would call the methods to change height and width multiplying the values by two, thus doubling its size. However, if this same method receives a Square as an argument, the behavior will not be the same, as the Square does not have the methods for changing height and width, so it will be necessary to validate the specific argument type received by the function to perform the proper behavior of doubling the shape’s size. Therefore, Square and Rectangle are not interchangeable, thus violating the Liskov substitution principle. In order to correct this problem, a new abstract superclass, or interface, can be created, called SquareShape, which has a method “defineSize()”, thus making the additional behavior be defined in each implementation in the most adequate manner.
This concept may seem a bit confused and hard to understand, but it was addressed implicitly when we gave an example for the open-closed principle (OCP). The area calculation class can receive any geometric shape subtype with no need to know which type it is receiving for its task to be performed successfully. In other words, not observing the Liskov substitution principle also entails violation of the open-closed principle.
Interface Segregation Principle (ISP)
This principle states that no code should be forced do depend upon methods that it will not use, thus dividing large interfaces into smaller and more specific interfaces where the client will only know the methods of its interest. The purpose is to reduce coupling between classes, thus facilitating the application’s refactoring and evolution.
If we return to the example mentioned in the OCP and add new geometric shapes susceptible of having their areas calculates, such as a cube, it does make sense to follow the same logic previously used by creating a new class for implementation of the standard geometric shape and naming it “cube”. Now let’s say that the system evolves again and we need to calculate the cube’s volume… To continue with the same logic, we should alter the interface “geometric shape” to have a new functionality that calculates its volume and consequently all its implementations would need to be changed too, to implement the new functionality. However, circle, square and triangle have no volume, as they are two-dimensional geometric shapes. It should be noted that this system evolution may have a significant impact (in terms of work volume) depending on the number of “geometric shape” implementations, not to mention the need to modify classes that initially were not part of the modification scope. This would lead the interface segregation principle to be violated, indicating an extremely coupled code that is hard to change. In this case, it is more interesting to use a new interface to represent three-dimensional geometric shapes, which in turn may benefit from behaviors already defined in the interface “geometric shape” through inheritance/polymorphism, so we can isolate specific behaviors without impacting other part of the systems that must not have direct relationship, thus reducing the coupling between classes and facilitating the code’s maintenance.
Dependency Inversion Principle (DIP)
The dependency inversion principle says that we must depend on abstractions rather than on implementations.
Again, let’s see the example of the class ” DatabaseConnectionManager” previously mentioned in the SRP. In case our system is using a PostgreSQL database, this class may tend to specialize in performing connections with this database and also have a little less intuitive name like ” PostgresDatabaseConnectionManager”. However, let’s say that in the course of the application development, the architecture team thought we’d have a significant gain if we used another database, MySQL, for instance, arising from the reason X, Y and Z… Here we would have to write a new class “MySQLDatabaseConnectionManager” that was capable of performing the connection to the newly chosen database and also change the dependencies in all parts of the application that used the old connection to perform some specific task, so they can start using the new class. (If we have too many classes using this dependency, can you imagine all the trouble?)
However, the example class “DatabaseConnectionManager mentioned in SRP was not chosen at random. This class easily represents the concept of an interface and “PostgresDatabaseConnectionManager” and “MySQLDatabaseConnectionManager” the concept of implementations. In other words, all the system parts requiring connection to a database need a dependency managing connections but not necessarily a dependency managing connection to a specific database. Suppose that a class of our system (“ValueCalculator”) is responsible for calculating an amount that will be then saved in a database: its sole concern must be the performance of the calculations even though it depends on another class that will communicate with the database to save these amounts. At this moment, “ValueCalculator” does not need to know details of the database communication, much less the type of database that will be used, do you agree? As we said before, its role is to perform the calculations. Based on this approach, we can them make it depend on the interface “DatabaseConnectionManager” and not necessarily on its implementations, thus making the utilization of the implementations interchangeable.
The five principles included in the SOLID acronym are concerned with dependency management in the software development and maintenance process, indicating points of attention and manners to circumvent possible issues already known by the community which may cause difficulty to programmers when changing or adding any new behavior to an application. The adherence to these principles makes the applications more robust, cohesive, reusable and easy to change and evolve.
Although they are relatively simple in their definition, their application opportunities can often be difficult to spot, especially for beginner developers. However, it is interesting to revisit these and other theoretical principles from time to time so that the experience may recall points in which we could have applied these concepts and others in which we can still apply them. Certainly the interpretation of these concepts will not be the same if you reread this publication again within 6 months.