SOLID (Object Oriented Design) Principles

This article attempts to describe the best technique for writing code that requires a minimum of changes to add/modify requirements that are easily scale-able and most importantly, reusable. This is where the S.O.L.I.D. principles and Design patterns are useful.

S.O.L.I.D. Principles

As a developer, we write a lot of code daily, with the satisfaction that we have written very good code. Next day, you come to the office and are told that the requirements from the client have changed or that new requirements have been provided by the client, in the existing functionality itself. So what do you do? You start analyzing your existing code, create a plan to modify it and get the work done, thinking that this will be it, until the next day, when the same thing is repeated. And now you think, what is the best technique you should use to write the code that will require a minimum of changes to add/modify the requirements, that is easily scale-able and most importantly, reusable. This is where the S.O.L.I.D. principles and design patterns are useful. Our focus for now will be only on the S.O.L.I.D. principles.

The term S.O.L.I.D. was introduced by Robert Cecil Martin, who is also known as Uncle Bob. S.O.L.I.D. is an acronym for the 5 principles. Each of these letters further have a 3-letter acronym, as per their names. These principles allow us to create a structure of code that is very easy to extend, re-use and make changes and may not break the existing code. So let's discuss these principles one by one. 

Single Responsibility Principle (S): It states that there should never be more than one reason for a class to change. In other words, a single class should be assigned only a single task to handle.

For example, while saving the registration data of a user, you might use the same class to send an email notification to the user, that you use to save the data. But if there is some change in the registration process of the data being saved or you are required to send extra information in the email, your changes can break or alter the existing functionality of the other component.

Open/Closed Principle (O): It states that classes should be open for extension, but closed for modification. In other words, in order to accommodate new requirements or modify the existing one, a class's code should not be modified, but rather be extended using the concepts of inheritance or abstractions. Of course, you may need to change the code to fix some kind of issues in functionality, but not for adding new functionality.

For example, you might use a logger class to log data in notepad files. But in the future, you may be required to use an XML based logger. Then you will be forced to re-write the logger class and handle the logging accordingly. To avoid this type of situation, you can use a base interface to represent the logging operations, and implement them as per various types you need. So this makes the logger interface closed for modification but open for extension.

Liskov Substitution Principle (L): It states that derived types must be completely substitutable for their base types. In other words, you can use a derived class instance instead of the base class instance, without affecting the output of the system.

To illustrate this, we will use a simple example of two classes of Rectangle and Square types. If we look at the hierarchy of these two, a Square is a type of rectangle but a rectangle is not necessarily a type of square. So by this we can say that, a Rectangle class can be considered to be a base class and a Square class can be derived from it. So when using instances of the Rectangle class, if we can replace it with the instance of the Square class without affecting the functionality or output of the system, then we can say it follows the Liskov Substitution principle.

Interface Segregation Principle (I): It states that clients should not be forced to depend upon interfaces that they do not use. In other words, interfaces should be separated in such a way that only the required client components are required to implement its operations.

For example, if we have an interface with operations to perform Registration and OrderPlacement, then we cannot have separate classes to represent the Registration and Order modules. This is because, if both the classes will implement the same interface, then the Order class will be forced to Registration functions and vice-versa, despite having no relations with each other. So a good solution will be to divide the single interface into two separate interfaces.

Dependency Inversion Principle (D): It states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

Basically, this principle means that communication between two modules of a system should be loosely coupled, using the abstract classes or interfaces and the higher level module should use these abstractions of lower level modules, rather than their concrete implementations.

For example, when using Logging operations (usually lower level modules) in CRUD operations (usually high level modules), they may directly use the logger class instances. This may result in future problems, when you may need to add a new logger type, say XML. Then you may be forced to change the existing code of a low level, in other words the logger module, to add a new type, resulting in violation of an Open-Closed principle. Using interfaces to implement the logger module can avoid such issues and is in sync with the Dependency Inversion principle.

S.O.L.I.D. principles vs Design

If you are new to the concepts of S.O.L.I.D. principles and design patterns and start exploring these, it might seem that both are the same. But they are not. They might sound quite close to these patterns, especially the Structural patterns. But they are different in their approach and their concept. One thing they have in common is they allow easy to maintain, re-usable and loosely coupled code.

One important difference, that I have seen is that these principles focus more on the extension of the existing code classes rather than the modification, that is quite clear from the use of the Single responsibility and the Open/Closed principles. On the other hand, using the design patterns more or less will result in the modification of the existing code and even sometimes you may find that design patterns may violate these principles, like, for example the use of the patterns like factory method or the facade pattern might result in violation of S.R.P. and O.C.P. So the more you study these concepts, the more differences and similarities you will find between the two concepts.

An important point of discussion now is whether to learn these design patterns first or these principles. In my opinion, go for these principles first. This is because, if you try to compare both of these, you might feel that these principles can act as the base of the design pattern, but not the other way around. You can extend your code much easily if you are using these principles, as compared to the use of design patterns. But this does not mean that you should not go for the design patterns. You can improve your code writing drastically using these principles and patterns but the point is that it will depend only on the situation that whether you should go for these principles or design patterns or both. Both of these are not must use, but they are aimed at reducing the common issues in our code like redundancy, complexity and tight coupling. I hope you enjoyed reading this.