Whether you are curious to know what SOLID in design principles means or you already know and interested to learn more, check out this article about these five principles and how they can be used to design object-oriented software systems.
Software design plays a crucial role in the development and maintenance of software systems. One may design a system with bad practices. Developers and Programmers would find it a nightmare to code and maintain such a system.
SOLID principles lay the foundation for good software design. They aim to reduce the design flaws and implement best practices in the software system.
In the year 2000, Robert C. Martin(Uncle Bob) introduced design principles in his paper “Design Principles and Design patterns”. SOLID is a mnemonic acronym introduced by Michael Feather for the five design principles it represents. These five principles are :
- Single Responsibility Principle,
- Open-Closed Principle,
- Liskov Substitution Principle,
- Interface Segregation Principle, and
- Dependency Inversion Principle
These principle are based on best practices in software design.
He recommends the adoption of best practices and design principles early in the software development life cycle. This would reduce or remove ‘The Symptoms of Rotting Design‘ in a software system. Failure, would cause ‘Design Smells‘ and would lead to a system with a huge pile of technical debt and possible costly redesigns.
The adoption of industry standards and best practices(design principles and design patterns) leads to a system with good design. Such a system would be reliable, maintainable, and testable.
#1. Single Responsibility Principle (SRP):
This principle states that a single responsibility of the system, should drive the design of a class or module. The design intent of the class or module must define its behavior(responsibility).
It is against the idea of a class or module having all or multiple groups of the behavior of the system. Such a design would make the system rigid, fragile, and less maintainable.
The class or module must perform one task or achieve a single objective. It should be a single cohesive unit of responsibility. The SRP promotes cohesion and follows the motto – “do one thing and do it well”.
Adhering to this principle in design of classes and modules, leads to separation of concerns(SoC) and maintainable code.
#2. Open-Closed Principle (OCP):
This principle is based on abstractions. It states that the design of a class or module must allow behavioral extensions with minimal code changes.
Extension of behavior and state requires code modification. This would lead to a codebase that is error-prone and less maintainable with a fragile system.
Abstractions aid code extensions by defining a hierarchy of classes, with the base class generalized to an interface or abstract class. There are two approaches to extend the behavior:
The first approach is based on the premise that behavior to extend is a family of algorithms. An interface can abstract the behavioral contract for these algorithms. Classes realize this interface to provide the behavior of the algorithms. The client can extend behavior dynamically, by composing this interface(strategy design pattern).
The second approach is based on implementation inheritance. The objects in the inheritance hierarchy are closely related and the behavior can be abstracted to a base (abstract class). The behavior in the base class is a series of common steps except for a few abstract/hook methods. The derived classes override these methods, to provide the required behavior(Template Method design pattern).
The use of abstractions and the Open-Closed principle leads to maintainable and loosely coupled systems.
#3. Liskov Substitution Principle(LSP):
This principle is based on behavioral subtyping. It states that in an Object-oriented system, a subtype can replace a reference to a base type without breaking it. This change should not make the system inconsistent and must be behaviorally correct (must have no effect on the program’s observable behavior).
A Subtype would be behaviorally correct, when the base class behavioral contract is honored by the subtype(Design by Contract). The behavior of the subtype and base type can have pre and post conditions that drive this contract. The following statement can best summarize the contract:
The subtype implements or overrides the behavior based on the design intent , honoring the design contract.
Thus, a module designed with LSP, base class references can be used safely as a place holder for the dynamic behavior of its subtypes.
#4. Interface Segregation Principle (ISP):
The intent of this principle can best be defined as “Clients should not be forced to depend upon interfaces that they do not use.”
Interfaces encapsulate behavior and define the contract that all implementing classes need to realize. A client to the interface, would expect the behavior to be specific to their requirements.
An interface that is a union( “fat interface” ) of all the behavior expected would be a disaster and violate the ISP i.e; “interfaces are not cohesive“. The interfaces need to be broken down as per the requirements of the clients.
One use case of this principle is where a client-specific “facade“ is provided by an interface. The interface decouples the client from its implementation. An example of this is when we interface to third-party software and expose only a subset of functionality. Component Object Model(COM) interface is a practical example of this type.
Another use case of this principle is to define a hierarchy of classes that can relate to a common abstraction. The classes down the hierarchy can now group or segregate with common behaviors, different from the abstraction at the top level. These group of classes can generalize their behaviors to an interface. The client can use these behavioral contracts (interfaces) based on their requirements.
An example of this second use case is all living beings can be abstracted to animals. The animals down the hierarchy can further be categorized(segregated), like flying and terrestrial. These behaviors would be ideal to be defined as contracts or interfaces.
#5. Dependency Inversion Principle (DIP):
High-level modules are dependent on low-level modules(utility classes) for their functionality. This principle does not recommend direct use of low-level modules in the high-level module.
Rather, it would be a better design to abstract the low-level module as an interface and use this as a dependency in the high-level Module. Such a design would reduce coupling and promote extensibility of the low-level module.
Further, the low-level module objects can be constructed by factories or provided by injectors (framework provided). They can be used to provide a specific implementation of the low-level module. This strongly decouples the implementation of the low-level module and its behavior.
SOLID is an acronym for five design principles discovered by Robert C. Martin. They are a set of rules and best practices to follow in the design of a software system. These guiding principles help to reduce the symptoms of bad design (rigidity, fragility, immobility, and viscosity) and promote good design practices.
The SOLID principles and GoF Design patterns are industry-wide best practices used in the object-oriented design of software systems.
I hope you enjoyed reading this article on SOLID design principles, please do share your views in the comments section. If you like the article, please do share it on social.