SOLID is a mnemonic acronym for five software design principles intended to make software design more understandable, flexible and maintainable. The principles are a subset of many principles promoted by Robert C. Martin (aka Uncle Bob). These principles are the central part of Martin’s book Clean Architecture: A Craftsman’s Guide to Software Structure and Design.
The following post is a result of Martin’s book summary with additional thoughts that came up during reading his book.
What is Software Architecture?
Every software system provides two different values to the stakeholders: behaviour and structure. Behaviour is the features provided by the software system. The structure is a set of restrictions and paradigms that allows keeping building new behavioural requirements in the same certain way. In other words, behaviour is the what part of the system and structure is the how.
Behavioural requirements are constantly changing, to efficiently deliver those requirements, software must have a restricted structure.
Traditionally, leaders and managers used to say, “I will tell you what to do, and you will tell me how you are going to do it.” The teaming of humans and machines, combined with the fast pace of innovations, has changed the order. This means as we discussed, that the “how” defines the “what” more than the “what” defines the “how”.The Human-Machine Team – Brigadier General Y.S, 2021, p. 102.
Having that said, software architecture is the set of paradigms and restrictions that compose the structure of a software system.
Good software architecture ensures that each change in the software system will take an effort in proportion to the size of the change.
Software architecture should not care about implementation details, the architecture objective is the separation of concerns which is the division of the software into layers.
As said above, the goal of software architecture is to keep the effort of requirement changes in proportion to their size. A good architecture allows working with incomplete knowledge, its function is to say how to not do things rather than how to do. An essential process to achieve this goal is layering the system components.
By separating the software into layers we produce systems that are:
- Independent of frameworks.Libraries and frameworks should use as tools, rather than having to cram the system into their limited constraints.
- Testable.Business rules have tests that are independent of UI, Database, Web Server or other components.
- Independent of UI.UI can change easily, without changing other system components.
- Independent of Database.The database can be switched from RDBS to NoSQL or any other DB system. Business rules are not bound to a specific database.
- Independent of any external agency.Business rules should know nothing about the outside world.
The SOLID Principles – Mid-level Architecture
The SOLID principles are intended to manifest mid-level code structure. The motivation behind these principles is the creation of mid-level software structures that:
- Tolerate change.
- Easy to understand.
- Basis of components to use in many software systems.
The SOLID principles are as follow:
- SRP – Single Responsibility Principle.
- OCP – The Open-Closed Principle.
- LSP – The Liskov Substitution Principle.
- ISP – The Interface Segregation Principle.
- DIP – The Dependency Inversion Principle.
Historically, the description of the single responsibility principle was as follow:
A module should have one, and only one, reason to change.
The reason to change is to satisfy users and stakeholders. Since a software system normally has several different groups of one or more people who require a module to change. The words “user” and “stakeholder” aren’t the right words to use. Instead, we’ll refer to these groups as actors.
Thus the final version of the SPR is:
A module should be responsible to one, and only one actor.
The SRP doesn’t mean that a function should do only one thing. Although it might be helpful to refactor large functions into smaller functions, this practice lives at the lowest levels, while the SRP is about the arrangement of the module level.
The term module refers to a cohesive set of functions and data structures. Cohesion is the force that binds together the code responsible to a single actor.
The appearance of this concept at the system components level becomes the Common Closure Principle. At the architectural level, it becomes the Axis of Change responsible for the creation of Architectural Boundaries.
The Open-Closed Principle says:
A software artifact should be open for extension but closed for modification.Bertand Meyer – Object Oriented Software Construction, Prentice Hall, 1988, p. 23.
The behaviour of a software artifact ought to be extendible, without having to modify that artifact. If simple extensions to the requirements force massive changes to the software, then the architects of that software system have engaged in spectacular failure.
The OCP more than it guides about how to organize classes and modules, it’s a principle to consider at the level of architectural components.
The accomplishment of this goal is done by partitioning the system into components and arranging those components into a dependency hierarchy that protects higher-level components from changes in lower-level components.
What is wanted here is something like the following substitution property: 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.Data Abstraction and Hierarchy, SIGPLAN Notices 23, 5 (May 1988).
In other words: a program should have the ability to replace any instance of a parent class with an instance of one of its child classes without negative side effects.
To demonstrate this principle, Martin uses the square/rectangle problem:
In this example,
Square cannot be an instance of
Rectangle because the height and width of
Rectangle are independently mutable; whereas the height and width of
Square must change together.
val square = Rectangle(10, 10) square.height = 5 square.area() // 50 square.isSquare() // false
The only way to ensure that
square is actually square is to add mechanisms such as
if statement to verify it. Since
Rectangle have different behaviour, those types are not substitutable.
The above example shows the LSP as a class inheritance principle. But actually, this principle is also applicable to the architectural level. Whether a software system provides classes and interfaces for other developers to instance it or services that respond to the same REST interface. The LSP is applicable because there are users who depend on well-defined interfaces and on the substitutability of those interfaces implementations.
A simple violation of substitutability can cause a system’s architecture to be polluted with a significant amount of extra mechanisms.
Make fine grained interfaces that are client specific.Principles of OOD – Uncle Bob
The motivation behind the ISP is to avoid unnecessary recompilation and redeployment of a module. If a module depends on code that includes functions it doesn’t use, a change of those functions will force a recompilation although nothing that the module cares about has changed.
The ISP suggest an interface layer between the definition class to its client implementation, the interface layer does not care about the implementation of the functions. Therefore, changes in functionality should not impact modules that don’t use it.
This could lead to the fact that ISP is irrelevant in dynamically typed languages since there are no such declarations in source code dependencies to force recompilation and redeployment. But in fact, this principle is also relevant on the architectural level.
On the architectural level, the ISP tells us to not depend on something (such as frameworks) that carries baggage that we don’t use.
The most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.Clean Architecture – Robert C. Martin Series, p. 105.
Source code dependencies should not refer to concrete modules. In a statically typed language, this means that import statements should refer only to source modules containing interfaces, abstract classes, or other kinds of abstract declaration. In dynamically typed languages, this rule applies that source code dependencies should not refer to modules in which the functions being called are implemented.
Treating this idea, as a rule, is unrealistic because software systems must depend on many concrete facilities. The implication then is avoiding depending on volatile concretions and favouring the use of stable abstract interfaces.