The SOLID principles have been described in detail in many different publications over the years. They themselves have been undergoing a lot of changes, shifts and some were even deleted. Until the final grouping stabilized in the early 2000s. So let’s find out what all the commotion is about, shall we?
Single Responsibility Principle – SRP
This is the most easily misunderstood of all the SOLID principles, the Single Responsibility Principle (SRP). That’s likely because it has a particularly inappropriate name. It’s too easy for programmers – me being the prefect example, to hear the name and assume that it means every module should do just one thing. Even though there is such a principle – A function should do one and only one thing. This is not the problem that SRP tackles.
Let’s analyze what SRP actually tells us:
A module should have one and only one reason to change.
Software systems are changed to satisfy users and stakeholders; those users and stakeholders are the “reason for change”. However a system will most likely have more than one user or stakeholder, who wants the system changed in the same way so we are really referring to a group. This group is commonly referred to as “actor“. So if we take another look at SRP it would now look like this:
A module should be responsible for one, and only one, actor.
Now, what is a module? – a cohesive set of functions and data structures. In some languages and development environments, this would be a source file. The word “cohesive” implies the SRP. Cohesion is the force that binds together the code responsible to a single actor.
Symptoms for SRP violation
Let’s take a look at an example of a payroll system and an Employee class from it. The class has 3 methods: calculatePay(), reportHours(), and save().
What would be the problem with this? Well, those three methods are responsible to three very different actors.
- The calculatePay() method is specified by the accounting department, which reports to the CFO.
- The reportHours() method is specified and used by the human resources department, which reports to the COO.
- The save() method is specified by the database administrators (DBAs), who report to the CTO.
Suppose that the calculatePay() and the reportHours() share a common algorithm for calculating non-overtime hours. To avoid code duplication the developers put that algorithm in a function named regularHours().
Five years later, the accounting department asks for a tweak in the way non-overtime hours are calculated. In contrast, HR department doesn’t want this tweak because they use non-overtime hours for a different purpose.
A developer is tasked to make the change, and sees the convenient regularHours() function called by the calculatePay() method. Unfortunately, that developer doesn’t notice that the function is also called by the reportHours() function.
The change is done, carefully tested, accounting department confirms the new behavior works as required and the system is deployed. Meanwhile, HR team has no idea about what is going on and so they continue to rely on the reports that are generated by the reportHours() function – but now they contain incorrect numbers. Eventually the problem is discovered, and the COO is livid because the bad data has cost his budget a lot of money.
Another symptom of the violation of SRP are merges. Let’s say the DBAs decide that there should be a schema change to the Employee table of the database. Suppose also that HR decide that they need a change in the format of the hours report.
Two different developers, possibly from two different teams, check out the Employee class and begin to make changes. Unfortunately, their changes collide. The result is a merge. We do have pretty good tools that help in resolving merge conflicts but this is still a risky affair. In our example, the merge puts both the CTO and the COO at risk. It’s not inconceivable that the CFO could be affected as well.
There are many solutions to this but all of them involve moving the functions to different classes. Most obvious solution is to separate the data from the functions. The three classes will share access to EmployeeData, which is a simple data structure with no function. The three classes are not allowed to know about each other. Thus any accidental duplication is avoided.
The downside here is that the developers now have three classes that they have to instantiate and track. A common solution for that is the Facade pattern. The EmployeeFacade would be responsible for instantiating and delegating to the classes with functions.
The best structure for a software system is heavily influenced by the social structure of the organization that uses it so that each software module has one, and only one, reason to change. Remember to separate the code that supports different actors.
Open-Closed Principle – OCP
For software systems to be easy to change, they must be designed to allow the behavior of those systems to be changed by adding new code, rather than changing existing code.
Let’s take a look at a simple example of an financial reporting system, where we are asked to make the reports available in different channels – web, mobile, PDF. To solve this we come up with the following:
The first thing to notice is that all the dependencies are source code dependencies. An arrow pointing from class A to class B means that the source code of class A mentions the name of class B, but class B mentions nothing about class A.
The next thing to notice is that all component relationships are unidirectional. The arrows point toward the components that we want to protect from change. If component A should be protected from changes in component B, then B should depend on A.
We want to protect the Controller from changes in the Presenters. We want to protect the Presenters from changes in the Views. We want to protect the Interactor from changes in – well, anything.
The Interactor is in the position that best conforms to the OCP. Changes to the Database, or the Controller, or the Presenters, or the Views, will have no impact on the Interceptor. Why should the Interactor hold such a privileged position? Because it contains the business rules. The Interactor contains the highest-level policies of the application. All the other components are dealing with peripheral concerns. The Intercactor deals with the central concern.
Even though the Controller is peripheral to the Interactor, it is nevertheless central to the Presenters and Views. And while the Presenters might be peripheral to the Controller, they are central to the Views.
Notice how this creates a hierarchy of protection based on the notion of “level”. Interactors are the highest-level concept, so they are the most protected. Views are among the lowest-level concepts, so they are the least protected. Presenters are higher level that Views, but lower level than the Controller or the Interactor.
The OCP is one of the driving forces behind the architecture of systems. The goal is to make the system easy to extend without incurring a high impact of change. This goal is accomplished by partitioning the system in components, and arranging those components into a dependency hierarchy that protects higher-level components from changes in lower-level components.
Liskov Substitution Principle – LSP
To build software systems from interchangeable parts, those parts must adhere to a contract that allows those parts to be substituted one for another.
The example – imagine we have a class named License. This class has a method named calcFee(), which is called by the Billing application. There are two “subtypes” of License: PersonalLicense and BusinessLicense. They use different algorithms to calculate the license fee.
This design conforms to the LSP because the behavior of the Billing application does not depend on which of the two subtypes it uses. Both of the subtypes can be substitute for the License type.
The LSP can, and should, be extended to the level of architecture. A simple violation of substitutability, can cause a system’s architecture to be polluted with a significant amount of extra mechanisms.
Interface Segregation Principle – ISP
Avoid depending on things that you don’t use.
Let’s imagine we have an Operations class and there are several users who use operations of that class. Let’s assume that User1 uses only op1, User2 uses only op2, and User3 only op3.
In statically typed languages like Java, programmers are forced to create declarations that users must import, or use, or include. These declarations create source code dependencies that force recompilation and redeployment. Thus, in our example, User1 will inadvertently depend on op2 and op3, even though it doesn’t call them. That dependency means that a change to the source code of op2 in Operations will force User1 to be recompiled and redeployed, even though nothing that it cared about has actually changed.
To avoid this we can segregate the operations into interfaces. In this case User1 will depend on U1Ops, and op1, but will not depend on Operations. Thus a change in Operations that User1 does not care about will not cause User1 to be recompiled and redeployed.
In dynamically typed languages like Ruby and Python, such declarations don’t exist in source code. Instead, they are inferred at runtime. Thus, you may start thinking that this is a language issue rather than an architecture issue.
In general, it is harmful to depend on modules that contain more than you need. This is obviously true for source code dependencies but it is also true at a much higher, architectural level.
Consider for example, we are working on a system, S. We want to include a certain framework, F. Now suppose that the authors of F have bound it to a particular database, D. So S depends on F, which depends on D. Any change done in D will force redeployment of F and, therefore, the redeployment of S. Even worse, a failure in one of the feature within D may cause failures in F and S, even though S doesn’t care about that feature.
Dependency Inversion Principle – DIP
The code that implements high-level policy should not depend on the code that implements low-level details. Rather, details should depend on policies.
The Dependency Inversion Principle tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
Every change to an abstract interface corresponds to a change to its concrete implementations. Conversely, changes to concrete implementations do not always, or even usually, require changes to the interfaces that they implement. Therefore interfaces are less volatile than implementations.
The implications is that stable software architectures are those that avoid depending on volatile concretions, and that favor the use of stable abstract interfaces. This implication boils down to a set of very specific coding practices:
- Don’t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, whether statically or dynamically typed. It also puts severe constraints on the creation of objects and generally enforces the use of Abstract Factories.
- Don’t derive from volatile concrete classes. This is a corollary to the previous rule, but it bears special mention. In statically typed languages, inheritance is the strongest, and most rigid, of all the source code relationships; consequently, it should be used with great care. In dynamically typed languages, inheritance is less of a problem, but it is still a dependency – and caution is always the wisest choice.
- Don’t override concrete functions. Concrete functions often require source code dependencies. When you override those functions, you do not eliminate those dependencies – indeed, you inherit them. To manage those dependencies, you should make the function abstract and create multiple implementations.
- Never mention the name of anything concrete and volatile. This is really just a restatement of the principle itself.
Good software systems begin with clean code. On the one hand, if the bricks aren’t well made, the architecture of the building doesn’t matter much. On the other hand, you can make a substantial mess with well-made bricks. This is where the SOLID principles come in.
The SOLID principles tells us how to arrange our functions and data structures into classes, and how those classes should be interconnected. 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 SOLID principles apply to those groupings.
The goal of the principles is the creation of mid-level software structures that tolerate change; are easy to understand; are the basis of components that can be used in many software systems.
Clean Architecture: A Craftsman”s Guide to Software Structure and Design by Robert C. Martin