SOLID - 5 principles of object-oriented design

SOLID principles - Everything solid?

The acronym SOLID is made up of the first letters of five principles of object-oriented programming:

  • SRP - Single Responsibility Principle
  • OCP - Open Closed Principle
  • LSP - Liskov Substitution Principle
  • ISP - Interface Segregation Principle
  • DIP - Dependency Inversion Principle

They were originally described in this constellation in the book Agile Software Development, Principles, Patterns, and Practices by Bob C. Martin. However, they are listed in a different order in this book. Michael Feathers later came up with the acronym when he realized that a different order would lead to this acronym.

The 5 principles of object-oriented design

The SOLID Principles are based on object orientation. This is most evident in the Liskov's substitution principle (LSP). It makes statements about inheritance. This only exists in the object-oriented paradigm. The other four principles also make sense in the other two paradigms, the Functional programming and the Imperative programmingoften also Procedural programming called.

Single Responsibility Principle (SRP)

I consider the SRP to be the fundamental principle of software development. It says:

A functional unit should only have one reason for changes.

This may sound a little confusing at first. Another definition would be:

A functional unit may only have a single responsibility.

But this definition is followed by the question of what is a Responsibility (responsibility)? I therefore find the first definition easier to implement. It is closer to my daily work as a software developer. Ultimately, the need for the SRP arises from the consideration that software must be changeable. New requirements are constantly being added that need to be implemented in the code. Changeability is one of the four values of the Clean Code Developer Initiative. To ensure that changes and additions can be made efficiently, each method and each class may only provide one reason for changes. This also clarifies what is meant by functional unit: methods and classes.

As soon as a method provides more than one reason for changes, it is responsible for more than one responsibility. In this case, changes for different reasons lead to a change to this method. On the one hand, this means that there is a higher risk of inadvertently making a change to the other responsibility. Above all, however, a method is more difficult to understand if it does more than one thing. And I shouldn't change things that I don't understand.

As a rule of thumb, I can recommend the following:

If you are in doubt whether a method only does one thing, break it down further.

This often raises the question of whether methods then become too small. From my many years of experience, I can say that small methods have never been a problem. They are focused and clearly understandable. This makes it easier to change and test them. And even the possibility of reuse is increased.

The SRP also results in an advantage for another value: the value of the Correctness. Small methods with clear responsibilities are easier to test automatically than long, impenetrable methods in which several aspects are mixed up.

Open Closed Principle (OCP)

The definition says:

A class should be open to changes, but closed to modifications.

In this definition, the second part is the relevant one. Being open to changes is not the problem. If new or changed requirements arise, the source code of the affected class can be changed. In this respect, every method or class is basically open to change. But: the second part says that these changes should be implementable without modifying the method or class itself. The source code should not be touched in order to make certain changes.

This already shows that the OCP does not appear to be applicable to every class. The vast majority of classes are not affected by the OCP. In order to be open to extensions, there must be a need for this flexibility. By far the largest number of classes do not require this flexibility.

The OCP makes sense if you are developing frameworks that are to be used in different scenarios. In particular, such frameworks are characterized by the fact that not all users are known. In the case of classes that are developed within a software system, all users are known: the software system! In this case, it is usually easier to modify the class directly if necessary, instead of introducing abstractions that would allow changes to the behavior without modifying the class.

One of the most important patterns for implementing the OCP is the Strategy Pattern. If, for example, the encoding used in a class for JSON serialization of objects is to be changeable, this aspect can be passed into the serializer as a strategy.

In the following example, an object of type JsonSerializerOptions a Encoder is defined. This means that a user of the class JsonSerializer is able to pass in the encoding strategy from outside. This means that the source code does not have to be adapted. As a developer, I could even go and implement and use my own encoding.

var options = new JsonSerializerOptions {
Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.Cyrillic),
WriteIndented = true
};

jsonString = JsonSerializer.Serialize(weatherForecast, options);

With the OCP, I actually have to issue a warning before using it:

If you are in doubt as to whether a class should comply with the OCP, ignore it.

Liskov Substitution Principle (LSP)

The definition says:

Inheritance must not restrict the functionality of the base class.

Named after Barbara Liskov, expresses the Liskovian substitution principle, which rules must be observed during inheritance. A user of the base class makes certain assumptions about the behavior of the class. Since a derivation is syntactically compatible with the base type, an object of the derived class can be presented to the user. However, the user still regards this as an object of the base class. If the derivation were to behave differently, the agreement with the user would be broken.

Very important in this context is the principle Favour Composition over Inheritance. If an existing functionality is to be reused, it must first be checked whether this is possible through the composition. The following pseudocode example is intended to illustrate this. If class B wants to use existing functionality from class A, it does not necessarily have to derive from it. Instead, it can gain access via the composition of objects. B uses A instead of deriving from it:

class A {
public int Calculate(int[] values) { ... }
}

class B {
private A _a = new A();

public int Calculate(int[] values) {
//...
var otherValues = _a.Calculate(values);
//...
}
}

In some cases, however, it may still make sense to work with inheritance. Since inheritance creates a very close dependency between the classes involved, this must be carefully considered. This may be the best solution for component libraries for graphical user interfaces. For domain logic within an application, it rarely makes sense to rely on inheritance.

My recommendation is therefore:

First check whether the desired functionality can also be created by composition. If inheritance proves to be the best solution, consider the LSP.

Interface Segregation Principle (ISP)

The definition says:

Interfaces should not force a user into dependency on details that they do not need.

The implementation of the principle is simple: separate the interfaces according to aspects. You could also say that the ISP is a special case of the SRP. If an interface has more than one responsibility, it violates the ISP. This is because using one aspect also forces it into dependency on the other aspect.

A classic example of this is a large interface IMultifunctionDevicewhich contains methods for printing, scanning and faxing. If a simple device can only print, it must still implement the entire interface and provide methods for scanning and faxing - even if these do nothing. This contradicts the ISP.

It would be better to split the interfaces:

interface IPrinter {
    void Print(Document doc);
}

interface IScanner { void Scan(Document doc); }

interface IFax { void Fax(Document doc); }

A device that can only print then only implements IPrinter. This means that it is not forced to maintain unnecessary implementations. Users are also not tied to functionality that they do not need.

Compliance with the ISP leads to clear interfaces and less coupling. Individual modules become more robust because they do not have to change if another responsibility within a previously shared interface changes.

My recommendation:

If you have the feeling when defining an interface that not every user needs all the methods - then split the interface.

Dependency Inversion Principle (DIP)

The definition is:

High-level modules should not be dependent on low-level modules. Both should be dependent on abstractions.

Abstractions should not be dependent on details, but details should be dependent on abstractions.

This principle is probably the most misunderstood of the five SOLID principles. It does not mean that there should be no dependencies - on the contrary: dependencies are necessary, but they should move in the "right direction" - away from concrete implementations and towards abstractions, usually in the form of interfaces. Furthermore, the DIP is unfortunately still too rarely scrutinized. There is an alternative with the IOSP!

Instead of allowing direct dependencies between concrete classes, according to DIP these should only be dependent on one interface. One class uses the interface, the other implements it. The concrete implementation is then integrated at runtime via dependency injection.

Here is an example:

interface IEmailService {
    void SendEmail(string to, string subject, string body);
}

class OrderService { private readonly IEmailService _emailService;

public OrderService(IEmailService emailService) { _emailService = emailService; }

public void PlaceOrder(Order order) { // Process order...
_emailService.SendEmail(order.CustomerEmail, "Confirmation", "Thank you for your order!"); } }

The class OrderService does not depend on a concrete implementation of e-mail dispatch, but on the abstraction IEmailService. This makes it flexible, easy to test and not dependent on technical details.

However, the class mixes OrderService aspects, as it takes care of processing the order on the one hand and initiates the email dispatch on the other. This problem can be solved by the IOSP.

My recommendation:

When you write a new class, ask yourself: Does it depend directly on concrete implementations? If so, add an interface and inject the dependency from outside.

Even better, however, is the consideration of the Integration Operation Segregation Principles (IOSP).

Writing clean code with SOLID?

The SOLID principles often appear to be the central principles for clean code. However, in my observations, they do not necessarily lead to clean code. This is because only one of the 5 principles is really relevant: the SRP. The other 4 principles are on a completely different level and are nowhere near as central as the SRP.

  • OCP: often leads to unneeded generalization and too much abstraction
  • LSP: only relevant for inheritance, which should be avoided anyway
  • ISP: important, but not a game changer
  • DIP: leads to many interfaces and tests with dummies. Overhauled by the IOSP.

Are the SOLID principles sufficient?

No, they are certainly not. As explained in the previous section, only the SRP is actually relevant. If the other 4 principles are not so central, the SOLID Principles may not be sufficient.

What additional principles are important?

I consider the following principles to be important as a supplement to the SRP:

  • IOSP - Integration Operation Segregation Principle
    The central principle for dealing with dependencies. It largely replaces the DIP.
  • DRY - Don't Repeat Yourself
    Duplication in the code is often the reason for errors and makes changeability more difficult. At the same time, the extraction of shared code sometimes leads to dependencies that also make changeability more difficult. Applied with a sense of proportion, the DRY principle helps.
  • YAGNI - You ain't gonna need it
    Only implement what results from the requirements. This leads to faster feedback and reduces the amount of work in the code. It often also makes the code simpler, because it does away with special features that were not required anyway.
  • KISS - Keep it simple stupid
    Implement in the simplest possible way. The same applies here: no special curls. Really good developers do not demonstrate that they have mastered all patterns and can build the most amazing abstractions. Instead, they implement the specific requirements in a simple and elegant way.

Conclusion

You can certainly discuss the choice of principles. In the end, I'm not interested in the one right choice. For me, the central idea is that the SOLID principles are guaranteed NOT to play the role that the acronym suggests.

Our seminars

course
Clean Code Developer Basics

Principles and tests - The seminar is aimed at software developers who are just starting to deal with the topic of software quality. The most important principles and practices of the Clean Code Developer Initiative are taught.

to the seminar "
course
Clean Code Developer Trainer

Conducting seminars as a trainer - This seminar is aimed at software developers who would like to pass on their knowledge of Clean Code Developer principles and practices or Flow Design to others as a trainer.

to the seminar "

Leave a Comment

Your email address will not be published. Required fields are marked *

en_USEnglish