In this article, we will explore the SOLID design principles and their architectural implications.
The SOLID design principles guide us on how to arrange our functions and data structures into classes. Now, what if a programming language does not have the concept of a class? A class is simply a coupled grouping of functions and data, so the principles apply to these logical groupings.
These principles enable us to create flexible software modules that tolerate change, are easy to understand and are reusable.
1. SRP: The Single Responsibility Principle
A module should have one, and only one, reason to change.
Note that the principle is not saying that every module should do just one thing, but rather, they have only one reason to 'change'. Software systems are subject to 'change' to satisfy users and stakeholders. In other words, a module should only serve one group of users/stakeholders.
What is a module? The simplest definition is just a source file and this definition suffice for most purposes. A module is just a cohesive set of functions and data structures.
As an example, given Employee class with 2 functions: calculatePay()
and reportHours()
. This module serves both the Accounting department and HR department. Accounting department only uses calculatePay()
while HR department only uses reportHours()
. Now, let's say both functions depend on a common method getHoursPerWeek()
. If Accounting department wants to tweak getHoursPerWeek()
, HR department is forced to consume that change too (even when HR do not want this tweak). This is violating SRP since any change requested by Accounting department may accidentally affect the HR department.
What can we learn from this principle?
Changeability - The SRP says to separate code that supports different users/stakeholders. This will help us create flexible systems that tolerate changes. We can apply this principle at the architectural level to create appropriate architectural boundaries.
2. OCP: The Open-Closed Principle
A software artifact should be open for extension but closed for modifications.
Imagine a system that displays a financial summary on a web page. Now we are asked to print the same information on a paper in a different format.
Clearly, some new code must be written. But how much old code will have to change? Ideally, zero.
How? By properly separating the modules that change for different reasons (the Single Responsibility Principle), and then properly organizing the dependencies between those modules (the Dependency Inversion Principle).
Note that generating the report involves two separate responsibilities: data calculation and data presentation.
We then organize the source code dependencies to ensure that changes to data presentation module do not cause changes in the data calculation module. In this way, we have extended a system to support paper-based format without modifying the current system.
What can we learn from this principle?
Extendability - The idea is to make the system easy to extend without incurring much change to the existing system. This is accomplished 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.
3. LSP: The Liskov Substitution Principle
Another way to think of the LSP is the "Plugin Architecture" that polymorphism provides in object-oriented programming. (Read more here)
In this architecture, modules can be substituted for one another as long as they adhere to the same interface.
The world of web is surrounded with interfaces in the form of REST APIs. Consider a food ordering service where a user can order from different restaurants in the same app. Internally, this food ordering service makes a GET
call to /order
URL. Each restaurant has different implementations for the order but they all expose the same /order
URL for the food ordering service to call. Food ordering service do not need to have restaurant-specific logic because of the well-defined interface. This is a good application of LSP where an interface enable substitutions of implementations.
Another example is when we write code to read from STDIN
, we are programming to the interface STDIN
. STDIN
can can be swapped with multiple device drivers implementations.
What can we learn from this principle?
Substitutability - LSP is applicable because there are users who depend on well-defined interfaces, and on the substitutability of the implementations of those interfaces.
4. ISP: The Interface Segregation Principle
The ISP states that we should avoid depending on things that we donโt use.
Going back to the Employee class example with 2 functions: calculatePay()
and reportHours()
. Let's say both the AccountingDepartment class and HRDepartment class imports and calls the same Employee class. Any change to one of the methods forces both caller to be recompiled and redeployed. Although AccountingDepartment class only uses calculatePay()
, it inadvertently depend on reportHours()
.
This can be fixed by creating 2 interfaces, one for each function and AccountingDepartment only interface with the one containing calculatePay()
.
What can we learn from this principle?
Avoid unnecessary dependencies to not overcomplicate system.
5. DIP: The Dependency Inversion Principle
The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concrete implementations.
In Java, this means that we only import
modules that are interfaces or abstract classes.
You might ask, what about String (java.lang.string
) class? String class is very stable with rare changes, so we do not have to worry about frequent and capricious changes to String. It is the volatile concrete elements of our system that we want to avoid depending on. Those are the modules that we are actively developing, and that are undergoing frequent change.
Interfaces are less volatile than implementations. Indeed, good software designers and architects work hard to reduce the volatility of interfaces. They try to find ways to add functionality to implementations without making changes to the interfaces. This is Software Design 101.
What can we learn from this principle?
Stable software architectures are those that avoid depending on volatile concretions, and that favor the use of stable abstract interfaces.
Conclusion
Interface! By creating well-defined interfaces that has only one reason to change and programming to those interfaces, it enables us to create appropriate architectural boundaries that allow our systems to change flexibly.
References: The content of this article is referencing concepts/ideas from the book Clean Architecture by uncle Bob (Robert C. Martin). It is a really great book that I recommend to further if you're interested about software architecture.
Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series). Buy it here: https://amzn.to/3SJguhf
The 3 Programming Paradigms
https://dev.to/brandongautama/the-3-programming-paradigms-34pe
Top comments (0)