Single Responsibility Principle in C#: How to Achieve This Holy Grail?
Single Responsibility Principle in C#: How to Achieve This Holy Grail?

Single Responsibility Principle in C#: How to Achieve This Holy Grail?

What’s your least favorite part of coding?

For most developers, it’s not the lack of coffee.

Or the late hours.

Or even the deadlines that make everything so stressful.

It’s the bugs. Specifically, the bugs that creep up when we do too much in a single method or a class. If you want to avoid bugs in a complicated code, you need to follow the single responsibility principle.

So what is the single responsibility principle? The single responsibility principle is a design principle which states that every class or module should have one responsibility and one reason to change. One reason for the change is a change in one set of requirements. This principle was introduced by Robert C. Martin (aka Uncle Bob).

This principle aims to solve the growing complexity of software and the difficulty of managing that complexity. The SRP is a guiding principle for creating quality software.

It is not a rule, and you can break it when necessary. Let’s first master it so we know when we can break it. You will also learn how to use the IMUM technique to split a single class into multiple classes.

What is the single responsibility principle?

You are a programmer (obviously).

You are working on a software application (captain obvious strikes again).

And let’s say you encounter a large class with many methods. It’s really long and complicated. Naturally, being a good programmer, you want to make this class smaller. But what methods to move to other classes? The single responsibility principle (SRP) is a rule of thumb that can help you answer this question.

It states that every method, class, or module should have one and only one responsibility. It also says that the class should only have one reason to change. In other words, a class should focus only on one logical function. This means that a class should not have:

  • methods that perform too many tasks,
  • methods that are overly coupled,
  • methods that contain duplicate code,
  • or other methods in which contain several hundred lines of code.

It sounds like common sense, but it’s surprisingly easy to violate this principle.

The Single Responsibility Principle (or SRP) is one of the first SOLID principles you should learn about in object-oriented programming.

SOLID principles

SOLID is an acronym that stands for five simple object-oriented programming principles:

  • Single responsibility principle – this post covers this SOLID principle.
  • Open-closed principle – classes and methods should be open for extension but closed for modification.
  • Liskov substitution principle – base class should be substitutable with subclasses without causing any side effects or compile errors.
  • Interface segregation principle – no class should be forced to implement a method in an interface that it doesn’t use.
  • Dependency inversion principle – the principle states that high-level modules should not depend on low-level modules. In other words, classes should depend on interfaces, not on concrete implementations.

What defines a reason to change?

In a way, the SRP principle sounds fair enough: each class should have one responsibility, one reason to change. And that responsibility should be clear, defined, and unique. But the reality is a lot messier than that.

Take a look at the following BookingService example:

public class BookingService
{
    public void CreateBooking(BookingData bookingData) { }

    private bool IsBookingValid(BookingData bookingData) { }

    public BookingData GetBooking(BookingData bookingData) { }

    public void PrintBooking(BookingData bookingData) { }

    public void DeleteBooking(int bookingId) { }

    public void SignUpUser(UserDTO user) { }

    public void SignInUser(UserDTO user) { }
}

It has the following methods:

  • CreateBooking
  • IsBookingValid
  • GetBooking
  • PrintBooking
  • DeleteBooking
  • SignUpUser
  • SignInUser

How many different responsibilities this class has?

The correct answer is: at least two.

The primary functionality of the BookingService class is to manage bookings. This public class has seven methods. Out of those seven, SignUpUser and SignInUser are not related to the primary functionality. These methods are responsible for managing user authorization. And those methods belong to a different class.

Ok, that separation was obvious. And what do I mean when I say that the class has at least two responsibilities?

Good question.

The other five methods belong to the same booking module. But do they belong to the same class? That is the question here. My answer is that those methods could be split into more classes. You will see in the IMUM section how to do that.

What happens if a class has more than one responsibility?

Often, classes have more than one responsibility. For example, a class that manages a cache and downloads data from a server. This class is likely doing too much and has too many responsibilities, many methods, and it’s probably not easy to test and change that class.

What can you do to fix this situation?

The answer is, you split the class into multiple classes! That sounds scary, but it’s actually not as bad as it seems.

You can do that with a refactoring process:

  1. Identify which methods belong to which responsibility – you can use Interface Method Usage Matrix to find out which methods should be grouped and moved to another class.
  2. Identify an existing class, or create a brand new class, where you will move those methods – use Extract Class refactoring to split the class into two classes.
  3. One by one, move methods and all dependencies that the methods use – use Move Method refactoring for this step.

So, now, the two classes have one responsibility each.

Why is the Single Responsibility Principle Important?

This principle is important for a few reasons:

  1. The SRP keeps a software project manageable by limiting the number of responsibilities developers have to deal with in a single class. It helps them to focus on the main task with a minimum of information to keep track of. In most cases, a single class should manage just a few responsibilities in a software project. This means you need to load less information about a single class into your brain, which is always a good thing.
  2. The SRP helps developers understand the dependencies and interactions in the code. You combine smaller classes using dependency injection. The code becomes more extensible and testable. Dependency injection helps you in building modular and reusable code. You can read about other dependency injection benefits in this post.
  3. The SRP helps developers understand the purpose of the code they are working with. It’s easier for a new developer to understand the existing code if it contains many smaller classes than fewer large classes. You can easier explain the system if it contains smaller pieces that work together to execute the business logic.
  4. The SRP makes software easier to maintain. The big class has multiple responsibilities. It can change because of multiple reasons. Every time you change one part of the class, you risk introducing a bug in the other part. If you put some of the responsibilities into a separate class, there is less chance that you will break the new class when you make changes to your original class. The sign of a good software design is when you can make changes to the application without breaking something.

How do you follow the Single Responsibility Principle?

There are three hard problems in software development: complex algorithms, naming things, and the single responsibility principle.

Look, I get it.

The SRP can seem like a generic guideline. But following this principle is not easy. It’s hard to know what is the class’ single responsibility.

So how can you actually follow it?

For a start, ask yourself the next question.

A simple question to validate your design

If you have a class, ask yourself the following question to validate if it breaks the SRP: What is the responsibility of this class? If the answer contains the word “and”, then the class is doing too much.

Let’s go through some examples to make this point clear:

  • This class calculates student’s grades, send emails AND saves calculation to the database – BAD
  • This class calculates student’s grades – GOOD
  • This class saves calculation to the database – GOOD
  • This class retrieves data from the database about the travel locations AND displays it on the screen – BAD
  • This class retrieves data from the database about the travel locations – GOOD
  • This class displays data about travel locations on the screen – GOOD

The other rule of thumb you can follow is this: if a class has more than 1000 lines of code, there is a high chance that it’s doing too much.

Don’t take it to extreme

The idea of SRP is that a class or a component should only have one reason to change. Unfortunately, many people don’t understand this and take the principle to the other extreme. They overuse it.

They have are too many classes that have only one method or two at max. Naturally, since you also follow the dependency injection rule, you have an interface for each class in your code.

This can lead to an explosion of interfaces. Every class has many injected interfaces, and managing the project solution becomes harder. Since every interface and every class has its own file.

Use Interface Method Usage Matrix (IMUM) to decide what to move

In this section, I will present the pragmatic way to decide what methods to move from the current class.

I call this approach Interface Method Usage Matrix (IMUM). Naturally, you would now expect a link to Wikipedia, or some official definition, or at least a link to a research paper from 1995. To back up my claims.

I have to disappoint you. Those things don’t exist. Yet. Since this is the approach I have invented.

(At least I think I have invented it. Maybe there is some other similar method, but it’s called totally differently? hmm…)

Let me try to come up with the official definition: Interface Method Usage Matrix (IMUM) is a matrix that consists of class’s methods on the Y-axis and class’s interfaces on the X-axis. By observing which interfaces are used in what methods, you can group the methods that carry the same responsibility. After that, you can move methods to another class. This leaves the current class with fewer methods and fewer interfaces.

Haaave you met IMUM?

Let’s go through one example to see how this method works in practice.

Imagine the following class, represented by the IMUM.

Simple IMUM matrix

In this example, the class has five methods: MethodA, MethodB, MethodC, MethodD, and MethodE. The same class has five dependencies: Interface1 through Interface5.

The matrix represents how the interfaces are used:

  • Interface1 is used in MethodA and MethodB
  • Interface2 is used in MethodA and MethodB
  • Interface3 is used in MethodC and MethodD
  • Interface4 is used in MethodC and MethodE
  • Interface5 is used in MethodA, MethodB, MethodC and MethodD

Ok, what does this mean? It means that there is a high cohesion between MethodA and MethodB. But also between MethodC and MethodD. If methods use the same set of interfaces, they tend to belong to the same responsibility. This may not be true in 100% of cases. But it’s a good indicator.

If I would decide to split this class, MethodA and MethodB would likely belong to one class. And the rest of the methods to the other class.

What about Interface5? This interface would be injected into both classes.

IMUM – BookingService class example

Ok, the last example was introductory. And meant to explain the mechanics of the IMUM technique.

Let’s go back to the original example, BookingService class, and represent the same class with the IMUM approach.

BookingService IMUM matrix

As established earlier in the article, SignUpUser and SignInUser don’t belong to this class. That was already apparent by their names. They belong to the class responsible for user authentication. The matrix states the same.

Next, examine CreateBooking, IsBookingValid, GetBooking, and DeleteBooking methods. They all use IBookingRepository, so they likely belong to the same responsibility. One note here: IsBookingValid also uses the IAccomomdationRepo. If, in the future, another interface is added to the BookingService class but used only in the IsBookingValid, this would mean that this method could be moved to the BookingValidator class.

And lastly, there is the PrintBooking method. It uses three interfaces: IUserDetails, IPrintEngine, and IVisualCalculator. This could mean that PrintBooking holds the responsibility that doesn’t belong to this class.

If you think about it, it makes sense:

  • Responsibility 1 – booking management – CreateBooking, IsBookingValid, GetBooking, DeleteBooking
  • Responsibility 2 – booking printing – PrintBooking.

Does this mean you need to split this class immediately? Not necessarily. If you don’t need to make any changes to the class, leave it as is for now. The next time you come to this class to make a change, consider whether you need to split the class first before making any additional changes to the code.

Conclusion

I have outlined some key points on how the single responsibility principle can lead to better software design. It is a software design technique where class, module, and method should have only one responsibility. This directly causes smaller classes, testable code, and improved maintainability.

The best way to achieve classes with fewer responsibilities is by using the following refactoring techniques:

I hope this article has helped decide how to implement single responsibility architecture in your application!

Disclaimer: The IMUM technique should serve as a guideline rather than as the definitive sign of how to split the class. Even though I have found that the technique holds true in my cases, I still have to investigate the IMUM further and back it up with solid evidence and more real data.