Single Responsibility Principle — Unclear Responsibility Boundaries

Sergey Radzishevskii
6 min readJan 16, 2021
Photo by Nathan Queloz on Unsplash

I assume that everyone knows about this very first principle in the SOLID abbreviation — Single Responsibility Principle(SRP).

Robert C. Martin expresses the principle as,

A class should have only one reason to change.

OR more detailed explanation

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Uncle Bob gives a really good explanation. And I highly recommend you watch this video if you haven’t seen it yet.

SRP may seem the simplest and clear, but is it really so? Does this statement sound simple enough for you? I bet it does. But I still see a lot of classes that violate the SRP. Let’s figure out when we violate the SRP and why. And will try to collect a set of recommendations on how to allocate responsibilities and not violate the principle.

Next, I will make you think with me. I will deliberately confuse you, so be prepared.

Let’s take an example

Let’s take some of the most common scenario — order creation. Imagine that we started working on a new project, the initial requirements that we have are as follows. In order to create an order you need:

  • validate user input
  • calculate the total amount
  • process payment
  • save the order to a database

Let’s forget good habits and create the simplest code possible — one function that does all the work.

ho-ho do not consider this code as production-ready. Demo purpose only 🤫.

Build SRP respectful code — use definition #1

Now let’s think about what the SRP-aware code would look like. Let’s get back to the original definition.

A class should have only one reason to change

I see 4 + 1 reasons to change. #1 The validation process can change — we may need to validate new fields, or we may change the validation rules. #2 The calculation process may change — we may include new fees or calculate a price depending on the subscription plan, etc. #3 We can change the way how we process payments, we can change the payment gateway from Paypal to Stripe. #4 We can change the way how we store orders - change Postgres to Mongo, save new fields, etc. #5 Finally we can change the original flow with new actions — we can notify the warehouse module about new payment.

If we follow the SRP principle, our code should look like this

I deliberately ignore other SOLID principles, in particular, the Dependency inversion principle.

If we change the validation rules we will change OrderValidator but OrderManager is protected from changes. There is only one reason to change OrderManager class — if the order creation algorithm changes(reason #5).

Why this approach looks too nerdy?

We have moved the implementations of the algorithms to external classes as the SRP rule suggests. But was it worth it? I would prefer to see the calculation of the total expanded, then I could immediately understand how we calculate it and I would not have to look into the external class.

What then are the advantages of using SRP? But let’s look at another definition of SRP.

Gather together — use definition #2

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Let’s look at an alternative scenario where the change can affect all functions. For example, we may need to add a voucher that can provide a discount for a customer. In this case, we will have to check that the voucher code exists and has not been used. The algorithm for calculating the cost will also change as it will be necessary to take into account the discount. Finally, we will also want to save the applied voucher information to the store.

Does this mean that we still keep all the code in one class? We can simplify the main function by breaking it down into smaller and simpler functions.

Perhaps this approach is the most optimal. The code is not very complicated but at the same time and explicit, i.e. it is easy to figure it out and see all the details without having to jump through other classes.

But does it mean that we should always use this particular approach, and not the previous one? NO — let’s figure out why.

Pros and Cons

So far, we have not considered one important aspect — the requirements will change. The readiness to adapt to change is determined by the flexibility of the code. And our examples have different flexibility. The first approach has the greatest flexibility, while it is devoid of explicitness because we abstracted and hid the implementation.

And this is where we have to be pragmatic. We need a balance between simplicity and flexibility. Finding this balance is not easy. Business requirements change and our code is constantly evolving. What looks simple today will grow tomorrow and will no longer be simple. The flexibility that seems overkill today may be needed tomorrow. But again, this does not mean that we should be ready for these changes today. Changes to the pricing algorithm may not change for years as long as we need to include vouchers or other factors. Here we can only rely on our experience and foreboding of changes.

Once again, each of the 3 options may make sense at different points in time. Most importantly (which Martin also mentions) we should always adapt and refactor our code. Don’t try to make your code perfect today, better adapt it to fit the new state tomorrow.

How can we adapt our code to fit simplicity and flexibility balance?

As we understand, predicting changes is not so easy in practice. Some changes can permeate the entire system, some relate only to a specific algorithm. The flexibility that is not needed at the moment can lead to complications.

Abstraction cost

Let’s look at one more issue — when should we separate a new abstraction. Look at this example again.

Explicit code looks simple and we know exactly how the calculation is made. It helps to keep the whole process in mind and understand what is happening at each step.

When we move the functionality into a new class(abstract things), we complicate our code. Yes, you understood everything correctly, now we need to know about the new class and call the required method of the new class. But if we remove a large enough chunk of code, it’s like removing -10 points of complexity and adding +1 point of complexity.

This is a very interesting topic but beyond the scope of this article. I recommend reading about Single Level of Abstraction and Memory Chunking vs Abstraction.

How can we separate responsibilities?

Often we can immediately identify responsibilities that absolutely have their own boundaries and roles that will influence them. But what to do with the functionality that is not so unambiguous or seems very small to be singled out as a separate responsibility?

As I mentioned, our code must always evolve to meet business needs. And this evolution is similar to our 3 examples:

  • step #1 — it could be inlined code inside the consumer’s class
  • step #2 — the consumer’s code has increased and we do not want to extend it further
  • step #3 — we receive requests to change the calculation algorithm, which do not affect changes within the OrderService

Again, these are not strict rules, you have to be pragmatic. But you have to change your code all the time to reflect your current business needs.

Smells that the SRP is violated

This is not a call to action, but a sign that something is wrong.

You want to test private/protected methods

When you want to test a private method, this is an indicator that the code in it has grown and it is difficult to cover all variations by testing the parent method. It seems that this code has already grown to a separate responsibility. This code should be moved to a separate class and then there will be no problems with testing.

Complexity balance is broken

Functionality in a separate method has grown too much

- OrderCreation
- createOrder
- input validation
-
-
- calculate the total amount
-
-
-
-
-
-
-
- process payment
-
-
- save the order to a database

calculateTotal method looks too big in comparison with other steps and complicates things when you need to focus on the process of order creation. Maybe it’s time to move calculateTotal to a separate class or at least a function.

The number of methods has grown too much.

Remember it’s hard for our brain to keep more than 9 things in mind

- OrderCreation
- createOrder
- calculateCommission
- calculateCommissionBaseAmount
- calculateDueNow
- calculateDueNowMarketplace
- calculateDueNowOnline
- calculateDueNowPartners
- calculateDuration
- calculateItemCommission
- calculateItemPrice
- calculateItemQuotes
- calculatePayoutAmounts
- calculatePayouts
.....

We can easily move out calculations to separate classes.

Conclusion

PSA is the most important of the principles that we must understand and use correctly. We must remain pragmatic and care about the balance complexity in our applications. Refactoring shouldn’t be painful, but it should be permanent.

Hope you find this article helpful. Please extend “Smells that the SRP is violated” in a comment.

--

--

Sergey Radzishevskii

Enjoy turning complex systems into intelligible architectures chilling in my garden.