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 central to 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, and it includes:
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 building new behavioural requirements in the same way. In other words, behaviour is the what part of the system and structure is the how.
Behavioural requirements are constantly changing, and 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 said that, 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.
What are Software Design Principles?
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 them. An essential process to achieve this goal is layering the system components.
Some of the most important principles to consider when creating software systems are:
- Modularity: Designing software systems to be broken into smaller, interchangeable components that can be reused in different contexts.
- Abstraction: Designing software systems to be abstracted away from the underlying hardware or operating system.
- Extensibility: Designing software systems to be able to easily add new features or functions without requiring significant changes to the existing system.
- Scalability: Designing software systems to be able to handle increasing workloads without requiring significant changes to the existing system.
- Security: Designing software systems to protect data and to remain secure in the face of malicious attacks.
- Performance: Designing software systems to maximize performance and efficiency.
- Simplicity: Designing software systems to be as simple and easy to use as possible.
- Readability: Designing software systems to be as readable, understandable, and maintainable as possible.
Formulating principles when creating software systems is essential to ensure the system is designed and built in a way that meets the needs of the end users and is as efficient, secure, and effective as possible. Principles can provide guidelines and constraints to help software developers make decisions during the design process, and can help ensure that the system is consistent, maintainable, understandable, and meets its goal. Without following a set of principles, software systems can become increasingly difficult to maintain and inefficient, resulting in higher costs of maintenance.
Layers of Concerns
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.
What are the SOLID Principles?
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 follows:
- 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 follows:
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 single responsibility principle states that every class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.
Example 1: A class called “Employee” should only contain methods and data related to managing an employee and nothing else. It should not contain methods to calculate payroll, generate tax forms, or any other unrelated tasks.
Example 2: A class called “DatabaseManager” should only contain methods and data related to managing a database and nothing else. It should not contain methods to generate reports, create user interfaces, or any other unrelated tasks.
The term module refers to a cohesive set of functions and data structures that are tightly bound together to serve a single purpose. Cohesion is the force that binds together the code responsible to a single actor.
The Single Responsibility Principle states that each component of a system should have a single, well-defined responsibility. This concept can be applied at both the system components level, where it is known as the Common Closure Principle, and at the architectural level, where it becomes the Axis of Change responsible for creating 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 how to organize classes and modules, it’s a principle to consider at the level of architectural components. It states that software components should be open for extension but closed for modification. This means that components should be designed in a way that allows them to be extended, but not changed.
Example 1: Instead of modifying existing code, you can use inheritance to allow for different implementations of a particular component. For instance, instead of changing the behavior of a Vehicle class, you can create a subclass of Vehicle called Car and provide it with its own implementation.
Example 2: Instead of making changes to existing code, you can use interfaces and abstract classes to allow for different implementations of a particular component. For instance, instead of changing the behavior of a Shape class, you can create an abstract class called Shape and provide it with an abstract method called draw(). Then, create a subclass of Shape for each type of shape and provide it with its own implementation of the draw() method.
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.
The Liskov Substitution Principle (LSP) states that objects in a program should be replaceable with instances of their sub-types without changing the correctness of that program. This means that any derived class must be able to completely substitute the parent class without introducing any new behaviors.
Examples of how to prevent a violation of the LSP include:
1. Ensuring that derived classes have the same methods with the same signatures as their parent class. This means that the same function calls can be used for instances of the parent and derived classes.
2. Making sure that derived classes only add more specialized behavior, rather than changing existing behavior. This means that any behavior already specified in the parent class must remain intact in the derived class.
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.
Example 1: An interface containing a large number of methods can be broken down into several smaller and more specialized interfaces. For instance, a separate interface containing methods specific to a certain type of user (e.g. superuser, administrator, or guest) would allow a client to only depend on the methods they need instead of the entire interface.
Example 2: An interface containing methods that are not related, such as methods for a payment system and methods for a user authentication system, can be broken into two separate interfaces. This allows the client to only depend on the interface they need, instead of implementing an interface with methods they do not need.
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.
By breaking down a large interface into multiple smaller, specialized interfaces, we avoid unnecessary recompilation and redeployment of a module. For instance, a separate interface containing methods specific to a certain type of user (e.g. superuser, administrator, or guest) would allow a client to only depend on the methods they need instead of the entire interface.
Similarly, an interface containing methods that are not related, such as methods for a payment system and methods for a user authentication system, can be broken into two separate interfaces. This allows the client to only depend on the interface they need, instead of implementing an interface with methods they do not need. By adhering to this principle, we can avoid unnecessary recompilation and redeployment when changes to a module occur.
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.
The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but instead both should depend on abstractions. This principle helps to reduce coupling between components, which helps to make code more modular and maintainable.
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.
Examples of how to prevent a violation of the DIP include:
1. Create an abstract interface that defines the structure of the code that both the high-level and low-level modules will use. This will ensure that both modules have a common interface that they can use to interact with one another.
2. Use an Inversion of Control (IoC) container to manage the dependencies between the high-level and low-level modules. This will allow the code to be more modular and maintainable, as it will be easier to swap out components without affecting the rest of the codebase.
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.
In summary, the SOLID principles are a set of five software design principles intended to make software design more understandable, flexible, and maintainable. The acronym stands for:
Single Responsibility Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
These principles are part of a larger set of principles promoted by Robert C. Martin, which are intended to help developers create better software architecture. The principles are intended to help developers create software that is more easily testable, independent of frameworks and UI, and independent of databases.
A blog post by Robert C. Marin – The Clean Architecture.
This blog post by Robert C. Martin is a must-read for anyone eager to learn about the fundamentals of architecture and design. As one of the most influential software developers of all time, Uncle Bob’s insights into the importance of clean architecture are invaluable to anyone who wishes to build high-quality software. He uses simple language and examples to explain the basics of clean architecture and the importance of designing code with scalability and maintainability in mind. This blog post is essential reading for any software developer, no matter what their experience level.
This book, Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin, is an essential read for any software developer looking to develop clean and maintainable code. It presents a comprehensive and actionable approach to writing software that is both maintainable and extensible. The book focuses on the principles and practices of software architecture and how to apply them to the software development process. With an in-depth look at architecture, the book covers the crucial topics of modularity, separation of concerns, and high cohesion. Additionally, the book dives deep into the practical techniques of test-driven development, refactoring, and design patterns. With its clear explanations, practical examples, and expert advice, this book is an excellent guide for any software developer looking to write clean and maintainable code.
The old and (still!) good design book – Design Patterns: Elements of Reusable Object-Oriented Software.
This book, Design Patterns: Elements of Reusable Object-Oriented Software, is the classic guide to software design patterns. Written by four experienced software engineers, the book has been praised for its concise and clear explanations of the various design patterns and for the practical advice it provides for using them in real-world applications. The book is a must-read for any software engineer who wants to understand and apply design patterns. With it, you can gain a deeper understanding of software design, which will help you write better code and make your projects more successful.