SOLID Principles – the well-defined path to the successful software architecture

SOLID is an abbreviation of 5 fundamental principles related to software architecture. Building your solution around the SOLID principles will reduce your application components’ dependencies and you will most likely make an easy-to-scale and extend solution. They are language-independent principles that can be applied easily in any OOP programming language.

Although the abbreviation’s arrangement is SOLID, there is no principle more important than the others. The arrangement is just for simplicity and pronunciation.

Single responsibility (SRP)

The first from the SOLID principles states that every object in our application, no matter if it is a class, a function, a library, a module, or anything else should have one and only one responsibility and all of that responsibility should be located inside that object.

The name Single responsibility easily suggests that the object must have only one purpose, but it is also important for this functionality not to be found anywhere else in the application. For instance – you are building an application for image processing, then:

  • All image processing methods like Crop, Rotate, Resize, etc. must be located in one place
  • All of the image processing functionality should be in that place

… anything else would result in a mess …

SRP – identify violations

Following this principle will lead to strong cohesion and loose coupling in the application. But what if you didn’t apply the principle, and how to identify violations? Here are a few examples:

  • One object should easily identify its purpose only by its name. If you are not able to understand what the object does only by its name, probably it does a lot and violates the SRP principle. For instance – the class ImageProcessorSaverAndLoader is not following the principle according to its name. This class may be responsible to the image processing operations like resizing, cropping, converting file formats, and so on. From the other hand it may also be involved in saving and loading the images to/from a database.
SOLID principles - Single responsibility - right and wrong class names
SOLID principles – SRP – right and wrong class names

We can easily separate the logic into two classes โ€“ ImageProcessor and ImageProvider. This way, we can easily decouple the different functionalities in two different objects.

  • Having a lot of different methods in an object – this is also a smell for SRP violations. Watch out for methods or properties not truly related to the object’s purpose โ€“ it is a classical sign of a violation. The class can be named ImageProcessor, but the logic inside can be for processing and persisting, which is the same problem, as the example above
SOLID principles - Single responsibility - right and wrong class methods
SOLID principles – SRP – right and wrong class methods

To avoid SRP violations always prefer many but small classes or interfaces, compared to the big ones. This will distinguish your application dependencies, and make your design more flexible.

Open-closed (OCP)

The principle states that our code should be open for extensibility, but closed for modifications. It means that you have to be able to easily add new functionalities without modifying the existing ones.

The principle is important if you are planning to extend your application in the future (and probably you will). Building your solution around this principle, will save you from possible breaks in your existing functionality when you add something new in a future release.

How to achieve the Open-Closed Principle

Relay on abstraction, not implementation!

If your class depends on another object to do its job, make this object abstract. For example – you have an application, in which you have logic for drawing a circle on the screen. For this purpose, you will probably have a Circle class and a Drawer class with a Draw method inside. The Draw method will accept the Circle object and it will do its job with the screen printing. But, what if you want to add another figure in the future? This will not work with a rectangle, for example:

SOLID principles - Open-Closed - rely on abstraction, not implementation - the wrong way
SOLID principles – OCP – rely on abstraction not implementation – the wrong way

If you want a rectangle you will have to change the Drawer because it is currently working only with a Circle. A better approach is for the Draw method to accept an abstraction, for example, an interface named IFigure. The circle will derive from this interface, and when you add a rectangle you will also derive from the IFigure interface. In this way the Drawer will work with any object inheriting from IFigure:

Open-Closed - rely on abstraction, not implementation - the right way
SOLID principles – OCP – rely on abstraction not implementation – the right way

The Drawer knows that it needs an abstraction of a figure to work. It can accept any figure, not only a specific one. You will be facilitated to add as many figures as you want in the future, as long as your Drawer is working with the abstraction IFigure.

Use abstract classes

Imagine, you have an abstract class Figure, with an abstract method CalculateArea. You create a Circle and inherit from the Figure, so you will be obligated to implement the calculation method, where you will use the specific formula for a circle area calculation:

Open-Closed - abstract class
SOLID principles – OCP – abstract class

When you decide to add another figure to your application, you can simply inherit from the Figure again. Then implement the new figure-specific logic for the area, in the new class.

You can also use virtual methods in your classes. For instance, using the previous example, you have a Drawer with a draw method. It is doing some specific jobs like printing on the console. You can create the Draw method virtual and implement your own Drawer, for example – WebDrawer, by inheriting the original Drawer. Then you will be able to override the Draw method to draw the figure in a web browser:

Open-Closed - virtual methods
SOLID principles – OCP – virtual methods

Liskov substitution

The principle states that every subclass must be a full deputy of its base class. Every subclass must be able to be used instead of its base class everywhere in your application, without breaking any functionalities.

This principle is closely connected with the Open-Closed, and achieving the Liskov substitution will help you to achieve the Open-Closed as well. To achieve this the subtype should not hide, or remove a base class functionality. When you make an application with some abstraction in it, for example, a school system, you will have an abstract class Person and also other classes like Student, Teacher, Director, and so on, which will inherit from the Person. Each one of these subtypes has an IS-A relationship with the person. The student IS-A Person, the Teacher IS-A person, and so on.

During the architecture design, you should also consider the IS-SUBSTITUTABLE-FOR relation type. This will help you to achieve the Liskov substation. The Student IS-SUBSTITUTABLE-FOR the Person, the Teacher IS-SUBSTITUTABLE-FOR the Person, and so on. Everywhere a Person is used it has to be easily replaced with one of its substitutes, without breaking anything.

SOLID principles - Liskov substitution
SOLID principles – Liskov substitution

Following this will help you have a clear design. There is no reason why the Student is not a Person. A code for printing all Persons in the School, must work well with the Teacher, the Director, and the Student.

Interface segregation

The principle states that we have to create small, cohesive, and well-defined interfaces, instead of big ones.

It is not required to combine all the class functionality in one big interface if you can make smaller ones. And also – always keep the public interface small and clear. If someone wants to use your code, he or she should be able to understand your interfaces pretty easily. Following the SRP will lead you to also follow the ISP.

SOLID principles - Interface segregation
SOLID principles – Interface segregation

Interface segregation – identify violations

  • A class with too many, not implemented methods – When you see such a class, it is a code smell for principle violation. Having big interfaces will make your classes include things that they may not need. And, if used by someone else, the such interface can mislead the client that everything from it is available (but it may not be).
  • Classes with lots of methods, but only a few of them are used – This can be a code smell for violation, since the class may implement a big interface with unnecessary things.

Dependency inversion

The last from the SOLID principles states that high-level modules should not depend on low-level modules. Both of them should depend on abstraction.

In many situations, our high-level application modules, depend on low-level modules to do their job. This will lead to serious coupling, and some modules may not work with anything else except a specific implementation. Designed like this the application is hard to extend. It would be difficult to make your modules work with other objects because they are already coupled with specific ones.

Please donโ€™t confuse the dependency inversion principle with dependency injection. The dependency injection is just a technique for achieving the DI principle.