This entry is part 3 of 6 in the series SOLID

SRP Definition

In this article, we will look at what is single responsibility principle and look at some simple examples in C# which demonstrate the single responsibility principle and how to adhere to the SRP.
Single Responsibility Principles or SRP in short states that every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class. This is the definition taken from Wikipedia. It’s very hard to understand what does it mean. For example, the first two questions which come to my mind are:

  • how to define those responsibilities?
  • how to calculate the number of responsibilities of a certain class?

Such definitions are not very helpful from the practical perspective. Robert Martin aka “Uncle Bob” clarified this definition by saying that

“There should never be more than one reason for a class to change”.

Sounds much clearer, though it still needs further clarification. The responsibilities of a class can be treated as axes of change. It might be simpler to understand it as axes of changing requirements. For example, if a class implements logging and caching on its own, then it has at least two responsibilities. We also can view the responsibilities of a class from the perspective of its users. Any API has its users. These users are the source of changes. Understanding that, we can calculate how many axes of change a class has. For example, if a class deals with a database, then we have one axe since DB architects are those users who can request changes. If that class deals with salary reports in addition, then we have two axes since accountants are those who can request changes to reports.

Apparently, the more responsibilities a class has, the more likely it’s going to be changed. So, applying SRP, we want to separate different concerns. A class should do one thing, and do it well!

By the way, the same principle can be applied at the level of modules. Modules should have only one logical responsibility. So, SRP can be applied at different levels: at the function level, object level and at the module level. Sometimes we find SRP violations at function’s level. We refactor out different responsibilities to classes, and then we separate classes by their responsibilities into different modules or assemblies if you wish. We discussed the SRP, but you might wonder how the concrete problems look like in real life caused by SRP violation.

Imagine the problems which are caused by SRP violation. Here we have a class. Look at it.

Calculating Responsibilities on the Example

[code lang=”csharp”]
public class PaymentProcessor
{
public void Charge(decimal amount)
{
//initialize bank terminal
//send a "ChargeCard" request to the terminal
}

public string CreateReport()
{
//format a report
return string.Empty;
}

public void PrintReport()
{
//initialize printer’s driver
//send a printing command
}

public void SavePayment()
{
//saving to DB
}
}
[/code]

How many responsibilities does it have? I see at least four dependencies. Let’s count them.

The very first method called Charge is responsible for interacting with a bank terminal; it should know how to initialize a device properly and send a corresponding command to it. The next method called CreateReport knows how to create a report about the payment transaction; it knows how to format all the data properly. Reports are usually so complex that actually here can hide two responsibilities. One separate concern could be the process of creating a report and the second is the formatting. If the process of creating an object is too complex that very often we treat it as a separate concern and we tend to isolate such responsibility by extracting a factory class which knows all the details about reports creation. The third method called PrintReport knows how to deal with printer’s driver and how to send commands to it. And finally, the SavePayment method is responsible for interacting with a database to save all the data about a payment transaction.

We often have a hidden responsibility in such cases. We can call that responsibility – “Orchestration”. We often need some high-level object which knows how to integrate all the responsibilities together.

Consequences

Now imagine that we need to change the payment processing and saving to a database simultaneously. Two different programmers are responsible for these two different parts of the system. Unfortunately, these two different parts reside in the same class. As a result, these two programmers need to change the same class, and in the end, they will need to merge their changes to finish a check-in.

Aside from this problem, such classes which accumulate many responsibilities are hard to understand. Such classes become a big ball of mud. One day, no one will understand what the hell is going on in that class.

Another problem is that sometimes we need to introduce changes into only one responsibility, let’ say we need to change the payment processing in the example I showed you above. Because several responsibilities reside in the single object, classes which represent them will also be recompiled and redeployed. Nowadays, this problem in the majority of cases may seem like not a problem at all. But for example, if we develop in C++ then it can cause some issues in the end. The thing is that such careless attitude to dependencies management leads to a long compilation time. C++ is much harder to compile, so this problem is very important in such cases.

So, when SRP is violated, responsibilities start to collate with each other, what means that they become coupled. What we need to strive to do is to gather all the same responsibilities together and separate from each other those which are different. When we gather the same responsibilities, we try to achieve the so-called high cohesion. When we separate different responsibilities, we try to achieve low coupling. At the level of functions, cohesion means the following: “a set of functions, an interface, is considered cohesive when each function is closely related to another.” Coupling indicates how dependent modules are on the inner workings of each other. Tightly coupled modules rely extensively on the specific state of each other, sharing variables and many types. Loosely coupled modules are fairly independent: they have a few well-defined APIs and share a limited amount of or no data at all. Let’s look at other interesting examples of SRP violation.

More Examples of SRP Violation

Here is the first example:
[code lang=”csharp”]
public string GetReport()
{
int clientsNumber = GetNumberOfClients();
decimal totalIncome = GetTotalIncome();
int satisfiedClients = GetSatisfiedClients();
int unsatisfiedClients = GetUnsatisfiedClients();

string clientsStr = $"Total number of Clients = {clientsNumber}";
string incomeStr = $"Total Income = {totalIncome}";
string satisfiedClientsStr = $"Number of satisfied Clients = {satisfiedClients}";
string unsatisfiedClientsStr = $"Number of sad Clients = {unsatisfiedClients}";

return clientsStr + Environment.NewLine +
incomeStr + Environment.NewLine +
satisfiedClientsStr + Environment.NewLine +
unsatisfiedClientsStr + Environment.NewLine;
}
[/code]
Here we have a method named GetReport. In the body, we can see that it gathers some statistical data, then creates formatted strings and put them together. How do you think, does this method violate SRP?
This method clearly violates the SRP. It has two responsibilities. The first one – gathering the statistical data and the second one is formatting. Junior programmers very often mix the formatting responsibility with business logic. Of course, sometimes, if we’re sure for 99% that formatting will never be changed and it doesn’t interfere with the other code, we can leave such code as it is. But more likely it would be better to at least extract formatting into a separate method.

Let’s look at another case.
[code lang=”csharp”]
public void FindAlarmDevice()
{
var driver = new AlarmDriver();
string port = driver.Find();
if (!string.IsNullOrWhiteSpace(port))
{
SystemState.AlarmCanBeUsed = false;
}
SystemState.AlarmCanBeUsed = true;
}
[/code]

Here we have the FindAlarmDevice. It scans through serial ports trying to find the device, and if it finds it, it sets a global state by setting the AlarmCanBeUsed property. How do you think, does this method violate the SRP?

This method clearly violates the SRP. It mixes the policy or business logic with mechanics. Remember that high-level policy should be decoupled from the low-level details. We could refactor it by introducing a special class which is responsible for interacting with global state. That class could receive notifications via events or any other appropriate way.

Let’s look at the next case.
[code lang=”csharp”]
public void DrawRectangle()
{
Rectangle rect = GetRectangle();
Color color = Colors.Red;
rect.Fill(color);
}
[/code]

Here we have the method named DrawRectangle. It calculates where to draw a rectangle, then picks a color and draws the rectangle filling it with that color. How do you think, does this method violates the SRP? This case is trickier than previous ones. The previous examples demonstrate very popular smells of violating the SRP. It’s harder to determine the responsibilities in this example. The number of actual responsibilities always depend on the users. Are there actors or users who might be interested in managing the color? It there are such users, then it might be better to separate the responsibility of picking a color.

A separate interesting question is whether the creation of an object is itself a responsibility or not? We’re not going to answer this question in this section. We will keep it for the section about DIP.

Here are the most common SRP violations:

  • Mixing logic and infrastructure, when business logic is mixed with views (see Model-View-ViewModel, Model-View-Controller, Model-View-Presenter patterns) or a persistence layer.
  • Class or a module serves different layers: calculates income and sends e-mails, parses XML and analyze the results.

Conclusion

  • I like the definition of the SRP from Uncle Bob, “There should never be more than one reason for a class to change”.
  • Applying SRP we want to separate different concerns. A class should do one thing, and do it well!
  • SRP can be applied at different levels: at the object level, at function level, and module level.
  • Classes which accumulate many responsibilities are hard to understand.
  • The number of actual responsibilities always depend on the users.
  • When SRP is violated, responsibilities start to collate with each other, what means that they become coupled.
  • At the same time when abusing the SRP, you can end up with too many small classes, and methods with business logic spread among them. In such a case, you might want to apply the Façade pattern.
  • Remember, that those modules which are expected to change frequently should be isolated from the other parts of the system. Concerning this problem, I also want to add that in general, I agree that using interfaces with a single implementation can lead to less maintainable code, but sometimes the isolation provided by such design is exactly what we need to isolate a module that changes frequently.


You can take the full video course “Software Architecture: Meta and SOLID Principles” with a huge discount clicking here.

Also, consider the option to become a patron on Patreon, thank you in advance!

Series Navigation<< Introduction to SOLID PrinciplesSOLID Principles: The Open/Closed Principle (OCP) >>