SOLID was introduced by Robert (“Uncle Bob”) Martin around 2002. Some ideas were inspired by other practitioners, but he was the first to codify them.
The SOLID Principles tell us how to arrange our functions and data structures into classes, and how those classes should be interconnected [ed. The use of the word “class” does not imply that these principles are applicable only to object-oriented software. A class is simply a coupled grouping of functions and data. Every software system has such groupings, whether they are called classes or not].
The goal of the principles is the creation of mid-level software structures that:
- Tolerate change (extensibility),
- Are easy to understand (readability), and
- Are the basis of components that can be used in many software systems (reusability).
If there is one principle that Martin emphasizes, it’s the notion that software is ever-changing. There are always bugs to fix, features to add. In his approach, a well-architected system has features that facilitate rapid but reliable change.
The SOLID principles are are as follows. The diagrams and examples are from Ugonna Thelma’s Medium post “The S.O.L.I.D. Principles in Pictures”.
“A module should be responsible to one, and only one, user or stakeholder.” – Robert C. Martin (2002)
The Single Responsibility Principle (SRP) claims to result in code that is easier to maintain. The principle states that we want classes to do a single thing. This is meant to ensure that are classes are focused, but also to reduce pressure to expand or change that class. In other words.
- A class has responsibility over a single functionality.
- There is only one single reason for a class to change.
There should only be one “driver” of change for a module.
“A software artifact should be open for extension but closed for modification. In other words, the behavior of a software artifact ought to be extendible, without having to modify that artifact. “ – Bertrand Meyers (1988)
This principle champions subclassing as the primary form of code reuse.
- A particular module (or class) should be reusable without needing to change its implementation.
- Often used to justify class hierarchies (i.e. derive to specialize).
This principle is also warning against changing classes in a hierarchy, since any behaviour changes will also be inherited by that classes children! In other words, if you need different behaviour, create a new subclass and leave the existing classes alone.
The Design Pattern principle of Composition over inheritance runs counter to this, suggesting instead that code reuse through composition is often more suitable.
“If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behaviour of P is unchanged when o1 is substituted for o2, then S is a subtype of T”. -– Barbara Liskov (1988)
It should be possible to substitute a derived class for a base class, since the derived class should still be capable of all of the base class functionality. In other words, a child should always be able to substitute for its parent.
In the example below, if Sam can make coffee but Eden cannot, then you’ve modelled the wrong relationship between them.
It should be possible to change classes independently from the classes on which they depend.
Also described as “program to an interface, not an implementation”. This means focusing your design on what the code is doing, not how it does it. Never make assumptions about what the underlying code is doing – if you code to the interface, it allows flexibility, and the ability to substitute other valid implementations that meet the functional needs.
The most flexible systems are those in which source code dependencies refer to abstractions (interfaces) rather than concretions (implementations). This reduces the dependency between these two classes.
- High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
This is valuable because it reduces coupling between two classes, and allows for more effective reuse.
In the example below, the PizzaCutterBot should be able to cut with any tool (class) provided to it that meets the requirements of able-to-cut-pizza. This might include a pizza cutter, knife or any other sharp implement. PizzaCutterBot should be designed to worwk with the able-to-cut interface, not a specific tool.