The SOLID principles are design principles that are used in software development. The term was coined by Robert C. Martin and describes five design principles that specify how functions and data structures are arranged in classes and how these classes should be interconnected.
The goal of SOLID is the production of software, which tolerates modifications, is easily understandable and which forms a basis of the components, which are used in many software systems.
In the following an overview of the principles as understandable as possible is to be given. I will not go too much into detail, since far more extensive publications and articles exist to the individual principles.
SRP-Single Responsibility Principle
The Single Responsibility Principle (SRP) is probably the most misunderstood of the SOLID principles. According to Robert C. Martin, software developers tend to assume that each module should perform a single task. The principle of refactoring and splitting to the lowest level for functions with a large scope does exist, but this is not what is meant by SRP. In general, the description of SRP is:
There should never be more than one reason to modify a class.
However, since in most cases software is written for customers/stakeholders or users and the notion of classes is too specific at this point, the following statement by Robert C. Martin is recommended:
„A module should be responsible for one, and only one, actor“.
As an example, one could take a module that provides functions for different business areas. If there is a change in one business area such that the structure of the class needs to change, this could impact the part of the class that is needed by another part of the organization. This creates dependencies that can be blocking for software development. Thus, it would make sense to design classes and methods in such a way that only one actor is responsible for the requirements and adaptations.
With the SRP it concerns thus mainly functions and classes and their connection to actors, which create requirements.
The Open-Closed Principle (OCP) was formulated by Bertrand Meyer:
A software entity should be open to extensions, but at the same time closed to modifications.
Or also: the behavior of a software entity should be so extensible that it does not need to be modified. One could now say that if a change to the software leads to the need for massive intervention in it, then the architecture of the software has failed.
What possibilities are there, then, to extend software without modifying it? When Martin adopted Meyer’s principle in the 1990s, he implemented it technically differently. For Meyer, the solution was to use inheritance, which is well known in the object-oriented world. For the time, this was an important factor towards maintainability and extensibility of software. Example: Suppose we have the two classes passenger car and sports car. The sports car class would inherit all important features and functionalities of the passenger car class. Specific functionalities and properties would then be added to the sports car class. The dependency here only goes in one direction, and that’s a good thing.
To take it a step further, you can extend this example even further with the use of interfaces. This is exactly what Martin does in his example.
OCP, in its extended version, is thus a useful principle that is widely used today. Although inheritance results in partitioning, there is no true multiple inheritance in Java, for example. Instead interfaces are used, which can be implemented again increased into a class. A goal and sense should be it always to protect classes of a higher hierarchy from modifications in classes of lower hierarchy.
LSP-Liskov’s substitution principle
LSP was defined by Barbara Liskov as follows:
If for every object o1 of type S there exists an object o2 of type T such that for all programs P defined in T the behavior of P remains unchanged when o1 is substituted for o2, then S is a subtype of T.
In principle, what is meant by this is that in inheritance, the subclass must contain all the properties of the superclass at all times, so that they can be used by the superclass. The subclass may not contain any changes to the functionalities of the superclasses. However, the superclass may be extended by new functionalities.
Let’s look at our car example again. We have the superclass car. This provides certain functions – such as acceleration and braking. But now we have two subclasses, namely sports car and small car. Both subclasses must be able to use the methods of the superclasses at any time. However, the subclass may have extended properties. For example, the sports car could still contain the function „Activate sports mode“, which manipulates the driving characteristics of the vehicle.
LSP thus goes a step further than OCP in imposing conditions on multiple inheritance to subclasses by the superclass.
ISP-Interface Segregation Principle
The ISP is used to avoid forcing users to implement parts of interfaces that are not needed. This should lead to the fact that interfaces do not become too large and that they do not shrink on a special use.
This can be easily seen in a graphical representation. The following figure shows that the superclass (class) has several operations implemented (Op1,Op2,Op3). However, User1 only uses Op1 and User2 and User3 only use Op2 and Op3 respectively. In this case, although the operation is not called, User1 would be dependent on Op2 and Op3. If, for example, something were changed in the implementation of Op2 in the superclass, then a completely new compilation and deployment would also have to be performed for User1. And this, although there is factually no change to the modules used by User1.
The solution to this problem would be to split the operations into interfaces, as shown in the next figure. In a statically typed language – say Java – User1’s source code would depend only on the User1Op1 classes and the associated Op1, and no longer on class.
The ISP is intended to prevent module dependencies that carry unnecessary load from causing completely unexpected problems. Mitigating dependencies ensures that code changes cannot lead to complex and extensive changes or problems. The extra effort of adding another layer after the fact ensures that the architecture is better able to handle modifications.
DIP Dependency Inversion Principle
The last of the five SOLID principles is the Dependency Inversion Principle (DIP). The DIP is intended to make it clear that systems are most flexible in which source code dependencies refer exclusively to abstractions, rather than to concretions.
In Java, it means that when using use, import, and include, statements should refer only to source modules-such as interfaces, abstract classes, or modules that guarantee any other form of abstraction. This is to enforce that there are no dependencies on concrete modules.
However, using this principle as a rule is anything but realistic, since software systems also depend on concrete entities. In Java, for example, the String class is designed to be very concrete. Trying to make it abstract would not make much sense. Here, the dependency on the String object should also not be avoided.
Because of this argument, DIP should mainly refer to the parts of the software that are being worked on and are obviously amenable to modification.
I would not call the principles the cornerstones of good software architecture, but rather a good foundation that every software developer and architect should have internalized.
You can see that some of the principles are recursively connected or build on each other. SOLID principles appear again and again with higher architectural topics and are thus important know-how for all beginning software developers and architects.