SOLID Principles

Achieve flexibility, sustainability, and ease of maintenance in software design with SOLID principles.

Adem Catamak
5 min readJul 12, 2024

Yazının Türkçe versiyonuna bu link aracılığıyla ulaşabilirsiniz.

SOLID represents the initials of five main principles established to ensure the quality of the software being developed. Software developed with attention to these principles will be flexible, sustainable, testable, and easy to understand.

We will examine the SOLID principles one by one. I will also provide examples based on the MarsRover problem.

Single Responsibility Principle

The essence of this principle is that a class or method should have only one area of responsibility. In other words, there should be only one reason for a class or method to change.

Let’s explain how we try to adhere to the Single Responsibility Principle by describing the responsibilities of a few classes in the MarsRover project.

The IVehicleBuilder interface has been created. There is a class named RoverBuilder that undertakes the skills of this interface. This class has a single responsibility, a single task that it knows. The answer to the question “How is a Rover produced based on the given argument?” is hidden within this class. If we want to change a step related to Rover production, we will need to intervene in this class.

Among the biggest enemies of the single responsibility principle are the words “and” and “or.” If we use “or” when thinking about the reason for making a change, our class does not comply with the Single Responsibility Principle.

Another example might be our class named VehicleFactory that exists in the system. We can see its Generate method below. If we look at this method, the only thing it does is find the appropriate person to make the vehicle based on the type of vehicle to be produced and transfer the necessary information to them.

Let’s imagine what would happen if the VehicleFactory were to process the parameters and produce the vehicles itself, and there were multiple vehicles (Rover, Drone, etc.) in the system. Whenever one of the steps in Rover production or one of the steps in Drone production changes, we would need to make changes to the VehicleFactory. In this case, the method would not comply with the Single Responsibility Principle.

Seeing these conjunctions in method names also indicates that the method does not comply with the Single Responsibility Principle. Methods like AddOrUpdate, IsNullOrEmpty, and SetAndGet do not adhere to the single responsibility principle.

Open-Close Principle

The openness and closedness mentioned in this principle mean “open for extension, closed for modification.” That is, we can extend our code to add new features, but we cannot modify it. To illustrate this principle, we can focus on the vehicles (Vehicle) part of the problem.

We introduced entities that have the ability to move with the IMovable interface. We designed our abstract Vehicle class to have the ability to move. As a concrete example of vehicles, we created the Rover class. Thus, we developed enough to solve our problem with the vehicles. Now we have a new task: sending vehicles that can fly to Mars. Let’s try to accomplish this task without changing our existing code.

We can take the first step by defining the IFlyable (flyable) interface.

We can see that the vehicles derived from classes that implement this interface have the ability to move up and down. Then, we can design a class named FlyingVehicle. Based on the obvious truth in the statement “Every flying vehicle is a vehicle,” we can establish the relationship as follows.

Finally, we can derive a class named Drone from the FlyingVehicle class.

Thus, we have enabled these vehicles to gain new features without making code changes in the system or altering the old behaviors of the vehicles.

One of the most important factors for the system to have backward compatibility is how well the Open-Closed Principle is adhered to.

Liskov Substitution Principle

This principle states that a derived class must inherit all the features of its base class. If it does not carry all these features, the designed class cannot be used in place of the base class.

To illustrate this, we can think of the Vehicle class. Instances of the Vehicle class have features like moving forward and turning left and right. When designing this, we assumed that no vehicle would be sent to Mars that couldn’t turn left or right. To continue our example, let’s imagine that we started building a railway system on Mars and designed a vehicle that no longer has the ability to turn left or right. In this case, we would see that our Train class, one of our vehicle types, does not behave like our other vehicles. According to our abstraction, the Train class is not actually a vehicle, or we would need to change our definition of a vehicle.

According to the Liskov Substitution Principle, if derived types have all the features of the base type, then entities can be used interchangeably. Taking the example explained in the Open-Closed Principle, we can explore the surroundings with an instance of the Rover class. Since the Drone class does not violate the Liskov Substitution Principle, we can also explore the surroundings with an instance of this class.

Interface Segregation Principle

This principle states that a class implementing an interface must fulfill all the requirements of that interface. It also indicates that interfaces should not have too many features.

For this example, let’s assume we designed the Drone class before the Rover class. Suppose the IMovable interface also has a Fly method. In this case, when we start designing the Rover class, we cannot find anything to put inside the Fly method other than the code line throw new NotImplementedException();.

Interface designs that violate the Single Responsibility Principle can prevent us from applying the Interface Segregation Principle. When we fail to adhere to the Interface Segregation Principle, we also violate the Liskov Substitution Principle.

Dependency Inversion Principle

The principle of Dependency Inversion aims to reduce the level of dependency between classes. By decreasing dependencies, this principle significantly enhances the quality of software in terms of maintainability and flexibility. One of its major benefits is seen in testability. If you encounter difficulties during unit testing, checking whether dependencies are adequately abstracted can be a good starting point.

Looking at the VehicleFactory class, it utilizes objects of type IVehicleBuilder through its own constructor method.

When we look at the Generate method, it contains a vehicle type and the necessary parameter data to produce this vehicle. When the method executes, it locates the IVehicleBuilder object of the appropriate type and then calls the object’s Build method.

If we had not inverted the dependency, the IVehicleBuilder classes (such as RoverBuilder, DroneBuilder, TrainBuilder, etc.) would have been instantiated inside the VehicleFactory class. In that case, VehicleFactory would have been tightly coupled to the Builder classes. The tests for the Factory class would also have tested the Builder classes.

By managing dependencies externally, we can model and test the situation as desired. Additionally, when testing the Factory class, using mock objects provided from outside means that the Factory class tests will not invoke real Builder class objects, keeping them isolated from the test.

Testing the scenario where a suitable IVehicleBuilder cannot be found would result in:

Testing the scenario where a suitable IVehicleBuilder can be found would result in:

We talked about the 5 principles that are important when doing Object-Oriented Programming (OOP) and that developers should pay attention to.

I hope it has been a useful post. You can click on this link to browse my other articles.

--

--