A more practical approach to the Single Responsibility Principle
“A Module should be responsible to one, and only one, actor.”--Robert C Martin
Uncle Bob himself admits that the Single Responsibility Principle (SRP) is often misunderstood. To make it easier to digest, he rephrased it three times since it first appeared in 2003. The industry, though, is still struggling to understand and apply it properly. I believe the reason behind this is that it lacks day-to-day practicality. Let's explain this...
The Single Responsibility Principle
Let’s focus on some code examples to understand the SRP better. Assume that we are working on an e-shop app. It sells products to both retail and business customers. We have a product class that looks like this:
The product has the title, desc, and price attributes. It also has two price calculation functions, one for business and one for retail customers. The business customers have different rules and discounts than retail ones. This class violates the SRP because those two functions belong to different actors. Different actors decide the price calculations for the two groups. As we will see, different regulations govern them as well. In other words, those two functions change for different reasons.
Let’s examine a symptom of the violation above. In every price calculation, we need to apply VAT. The VAT calculation is pretty simple; we just need to increase our final price by 24%. Therefore, to avoid duplication, we have implemented a shared function called applyVat:
Now suppose that the regulations in our country or state change. The regulators decide that the VAT for business-to-business transactions will drop to 12%. So the development team working in the business department finds it convenient to change the applyVat function. It’s as simple as changing a constant integer value from 24 to 12. It gets implemented fast and deployed to production to comply with the new regulations. What happens the next morning is that our retail customers are happily purchasing everything 12% cheaper than before.
How to comply with the SRP
There are multiple ways to refactor the code above to comply with the SRP. All the solutions involve segregating the business from the retail behavior. Each should be contained in a dedicated class.
We keep the signature of the functions inside the Product class while moving the actual implementations to collaborative calculators. Thus, changes in the business price calculation will not affect the retail price calculation.
In my experience, many engineers still find the SRP ambiguous and confusing. There are a lot of interpretations of what constitutes a “responsibility” and how to define if a module is pertaining to just one. It seems evident in carefully picked examples found in textbooks and blog posts. However, in day-to-day work on a production application, it feels hard and impractical to identify the actor of every module/class. It definitely is an important principle and deserves its place in S.O.L.I.D, but it is a bit abstracted, and it’s not always clear whether we are violating it.
Its connection with “actors” who are real people makes it ambiguous. Usually, we receive requirements from a single Product Owner. Their job is to abstract the complexity of where the requirements are coming from. It is not practical to think about every feature who may actually be the real stakeholder and possibly request changes.
The boundaries are often not so well-defined. In fact, it took me quite some thinking to come up with an example to describe the principle. Most blog posts use the same example Uncle Bob first used in the original post. In cherry-picked examples, it makes perfect sense, and it’s crystal clear. In day-to-day work, you can give it a try and draw your own conclusions.
Besides its ambiguity, I believe it still delivers value. It serves as the “business representative” in technical decisions. It reminds us, engineers, that not all technical decisions need to be driven by technical principles. The people/actors who drive the product decisions should also be considered while designing technical solutions.
Personally, what helps me more to define architectural designs in day-to-day work, is what I like to call the Single Concern Principle (SCP). The drivers behind the SCP and what constitutes a reason to change are technical rather than business-oriented. They don’t tie up to actual people. I will do my best to describe it below, and hopefully, you will also find it helpful while designing your application.
The Single Concern Principle (SCP)
The SCP states that:
Each software unit should have one, and only one, technical, fine-grained concern.
To make sense, we should define what a technical concern is. A concern is either:
The execution of a task or a group of related tasks.
The delegation of a task or a group of related tasks.
When working with Functional(like) Programming, we can also include that a concern can be:
Containing data that describes a business entity. This does not apply in pure OO designs as data and operations (task executions) sit together by the Data Encapsulation Principle.
With the term “task,” I refer to any calculation the software needs to perform. Like:
Applying business logic to solve a problem.
Data accessing or modifying. Either locally or remotely.
Drawing UI on a screen. Etc.
Using the above, we can categorize our classes or files into the following categories. Α class can be either:
An executor is a “class” that executes a task or applies logic.
A delegator or orchestrator. It orchestrates the flow of control and delegates the tasks to the responsible executors.
In pure functional programming, a data container. Those are our models, POJOs, or data classes.
If a class belongs in more than one category, it has more than one concern and violates the SCP. Another way to break the SCP is if it’s a container, delegator, or executor of tasks that are not related.
Again we fall into the same ambiguity we fell into before with the Single Responsibility Principle; what does “related” mean? We can use the same explanation; related tasks are tasks that change for the same reason. But here, we don’t tie this to actual people, roles, regulations, or anything related to the real world and business requirements.
The SCP stays at the technical level, where things are more straightforward. So how do we define that a group of tasks is related, they have a single concern, or otherwise change for similar reasons? The best way to understand this is by example. The example should be as generic as possible and not outline a specific scenario where the principle makes particular sense.
Let's explain how the SCP can help us evolve the architectural design of a mobile app.
In the initial architecture, we can identify the UI layer as an executor. It carries out all the screen interactions, whether it’s an iOS View Controller, an Android Fragment, or a Flutter Widget. Those tasks include drawing the UI as well as receiving user input. Those are related tasks; thus, they constitute a single concern.
The View Model is an orchestrator. It usually facilitates the communication between the UI and the Service.
The second SCP condition suggests that a piece of software must execute or delegate related tasks. The View Model belongs to the presentation layer, whose purpose is to serve the view layer. Consequently, one View Model should be responsible for serving a single screen. A single screen can be apart from multiple Fragments or views, but it has to be a single screen. A View Model serving multiple screens falls under the accidental duplication that we discussed earlier. The two screens change for different reasons. Thus it will cause the symptoms described earlier. The relationship between a screen and a View Model should be 1-1.
Finally, the Service is an executor. It is responsible for performing all the CRUD operations on our backend.
It’s important to understand that the SCP is a low-level principle that applies to the class level. The service layer can be broken down into multiple components like the HTTP client, the JSON parser, and the higher-level API. In this breakdown, the high-level API layer would be an orchestrator. The HTTP client and the JSON parser would be executors as they perform a specific cohesive task. The diagram below depicts the full breakdown:
Later in our story, we receive new requirements and identify the need for a database. Our first thought is something like this:
In this example, the service is responsible for two beefy concerns. One is to communicate with our backend, and the second is to execute queries and communicate with the database. This is a severe violation of the SCP, so we immediately refactor to:
Communicating with the database is moved to the DAO layer. This is an orchestrator; it provides an interface to interact with the DB. To comply with the SCP, we also include a separate lower-level DB client class. This is responsible for executing the actual queries. Thus, it’s an executor.
In the example above, we can identify that all the classes have a single concern except the View Model. The View Model now also contains the offline logic, whether we have to fetch fresh information from the Service or reuse the local data from DAO. This concern is pretty beefy, and its implementation can become quite complex. Therefore, we must refactor our system to include another orchestrator that will take this concern away from the View Model. Furthermore, we need a dedicated component to take care of the presentation logic. This will be the Presentation Mapper. Our architectural design now looks like this:
It’s important to state that from the View Model and left, the layers serve the users of our app. While from the Repository and right, they serve the data. Therefore, we need to have a single View Model per screen and a single Repository per entity. For example, we have an ArticleRepository responsible for data operations related to articles. And an AccountRepository responsible for data operations related to the Account entity. A repository that mixes unrelated entities