If you are one of this blog’s readers, you are probably familiar with the SOLID principles for OOD software development. The one thing you have never been taught, though, is how to turn those principles into a working methodology that is way simpler and much more intuitive than memorizing names of principles by initials.
If you are not familiar with the term, a short explanation will follow immediately.
If you do know the term, but are trying, right now, to remind yourself what, the hack, means one of those S.O.L.I.D. letters that you have just forgot – this post is exactly for you.
And if you are one of those that not only know what those five principles are, but also implement them on your code on a daily basis – I hope that this post will provide you a new point of view that will make it easier to teach it to the juniors you mentor in your team.
The Five Principles of Uncle Bob
The five principles were coined at the beginning of the millennium by Robert C. Martin (“Uncle Bob”), one of the initiators of Agile Development, and thanks to Michael Feathers got their catchy name “SOLID”, that represents them – one letter for each principle. Martin had been provided consultation to large firms, like Xerox and others, and while doing that, he had stumbled upon code “deceases”. Many of those issues were not related to wrong coding of specific modules, but rather to the lack of a general methodology for creating code that can easily evolve with the natural change of the system requirements. The goal of those principles is to provide the design – and, as a consequence, the coding – that allows efficient maintenance of the system and the codebase for a long period. The principles are general, and can be implemented on any system.
Those five principles do create a catchy acronym, but on their own, they are somewhat less catchy… Here they are:
SRP: The Single Responsibility Principle (S)
OCP: The Open/Closed Principle (O)
LSP: The Liskov Substitution Principle (L)
ISP: The Interface Segregation Principle (I)
DIP: The Dependency Inversion Principle (D)
What, in practice, each of these principles means? Following is a brief explanation.
The Single Responsibility Principle
The Single Responsibility Principle dictates that a single class should take care of a single “problem”. If we design a class that is responsible for more than a single task – we design it wrong. In the original view: Each class might have only a “single reason” for a change when the system evolves.
Thus, for example, if we design a class that reads and parses an XML file and produces a summary report in HTML – we assign too many responsibilities to a single unit. If, at some point in the future, the input file format will be changed – we will have to refactor the class, and might accidentally break the reporting code, that requires no change. If, on the other hand, the report schema is redefined – we will have to change its code, possibly jeopardizing the XML parser, although the last is completely agnostic to the requirement change. Even without breaking irrelevant pieces of code – a new compilation, linkage, testing and distribution might take place where they are not needed at all.
Therefore, if your design assigns to a class more than a single, clearly defined responsibility – it should probably be broken into several independent classes. That way you are improving encapsulation, reducing dependencies, and making sure that future changes will not affect irrelevant code.
The “Open/Closed” Principle
This is, in fact, the clear distinction we must draw between the aspects we decide to make fixed in our current code – the class invariant (and above all: The interface) – and the aspects that we deliberately open for later evolution of the system. The original definition says that our software entities should be “Open for Extensions but Closed for Modifications”. Throughout the evolving of object oriented programming mechanisms, that principle got a somewhat different flavor than the exact way Martin defined at first, thanks to polymorphism abilities. We can, therefore, treat it that way: The interface towards derived classes must clearly define what parts they are allowed to extend.
The Liskov Substitution Principle
That is one of the most fundamental principles in the OOD methodologies, and yet, even experienced developers except it from time to time without noticing. The principle is simple: Any type (class) that inherits another type, must be substitutive to that type, so that if class B inherits class A, then anywhere in the code where an object of type A is expected, we can provide a B object without changing the system behavior. This is known as the requirements of having “is-a” relation between a parent class to its inheritor – if B inherits A, then B is A.
Liskov’s requirement is stricter than the way most developers understand it. She requires – in addition to avoid a behavior change as in the classic examples of rectangle/square or ellipse/circle, two additional important requirements:
Exception throwing is part of a function’s signature. Therefore, an overriding function in an inherited class must not throw any exception that could have not been thrown from the original function in the parent class (unless that new exception itself inherits an exception type that the original function could have thrown; you can see an example for that principle in my post about exception handling).
Preserving the history of changes. If class B inherits class A, it must not change the internal data members it had inherited from A in such a way that an original A object could never have. In other words, it is never allowed to have the “A parts” of a B object found in a state that they will never be if they were part of an A object. Data members that are only found in B objects, of course, might be changed in whatever way required.
The Interface Segregation Principle
Any client of a code entity must only know the functionality it requires. For example, think of a product record in a data-base. That record might hold data about the product details, number of items in the inventory, manufacturer and many more. ORM systems allows us easily representing each record as an object of the relevant type. A trivial solution will be, therefore, to allow each client querying the object throughout that type. Thus, for example, purchasing department will be able to access the object in order to get the manufacturer details, while the e-commerce site will use it to know whether there are items in the inventory. Trivial – but not very successful, as Martin had understood when he helped Xerox to get out of a serious software crisis.
In such a case, it is important to provide different interface to each client. An interface for the web-site, for example, will allow getting details that are of interest for the prospect customer, stating whether there are items in the inventory and allow ordering the product. Another interface, for the management system, will allow getting the manufacturer details, and so on. That principle, that was written in the blood of Xerox multi-purpose machines, emphasizes the less intuitive understanding: It is better to have many interfaces, even very specific ones, than having a single interface that is generic and almighty. Anyone who knows even a few design patterns knows, that many patterns deal solely with interface definition. The purpose of all of them is the same: Separating the dependencies between entities that are not necessarily related to each other.
The Dependency Inversion Principle
Its name is a bit confusing, since in any software system we prefer not having dependencies at all… But there is a reason for that. In a “trivial” system, even if there is a clear distinction between the responsibility of each tier or layer, there is still a strong dependency between them. That dependency is usually a “top-down” one: If a system is composed of a few applications, it has to know them. If each such application uses several software elements, it has to know them. If each such an element uses a few utility libraries or drivers, it must know them. In such a system, even though the code might be well written and the responsibility might be well defined – it is still very difficult to reuse some parts of the code, or to replace elements in the “low level” of the system, without breaking the higher tiers.
The Dependency Inversion Principle states that such a dependency is wrong, and therefore, we must separate the dependencies using interfaces. Thus, for example, the system will know the interface to each application, but not the application itself. If, at some point, there arises a need to replace that application by another one, which provides a similar service using a different implementation – it will be very easy to do so. In the same way, the application will be no longer dependent in its utilities and libraries, but will only know their interface; at any point it will be possible to replace them by others, that provide similar services.
Well… Why “Dependency Inversion”, then? We are trying to completely avoid dependencies, aren’t we!? The reason is that a simple, common implementation of that principle is writing an interface (or abstract classes) in the “upper” tier, which is then being implemented in the lower tiers. That way, for example, the application defines the interface it requires from each software utility it uses; in the utilities tier, each utility will implement that expected interface. What we get is “inverted dependency” – the lower layer must know the upper layer in order to get the interface details. An even better approach is one that completely extract the interface definition from both layers. Thus, we will have an independent “utility interface” tier, that the libraries modules will implement, and the application modules will call and use.
So, What, in Fact, we had Here?
We have gone through five most-important principles, that we must consider upon the design or coding of any software system. It is possible to try to remember them using the acronym S.O.L.I.D., but I assume that even whoever remembers that L stands for “Liskov”, might not necessarily remember immediately the meaning of that principle. I suggest, therefore, to focus the actual meaning of those principles, in a way that allows both a clearer view and understanding of them, and a simple, intuitive method for considering those “Five Commandments” during any design, coding or review.
Each class has five important aspects:
- The most fundamental: What is its purpose?
- What does it define for future inheriting classes, if at all?
- What are its commitments in regard to the base classes, if there are such?
- What interface does it provide to its clients?
- Using what interfaces does it call other classes, as their client?
Graphically, we might look at those aspects this way:
Not surprisingly, the five SOLID principles consider exactly those aspects of a class. If we “put” each principle in the appropriate place of that schema, we will get something like this:
Now, the rationale of those five-rules-system makes much more sense, and even more important: We can easily remember it, by considering all the class’ interfaces – “Internal”, to the basic goal of the class, “Up” – to the parent base class, “Down” – to inheriting classes, “Left” – the interface exposed to other clients, and “Right” the interface for using other components.