NDepend Blog

Improve your .NET code quality with NDepend

SOLID Design in C#: The Single Responsibility Principle (SRP)

November 9, 2023 7 minutes read

Single Responsibility Principle in Action

The Single Responsibility Principle (SRP) is one of the five essential SOLID design principles. These principles are guidelines for the proper usage of object-oriented features. The SRP definition is:

A class should have a single responsibility and this responsibility should be entirely encapsulated by the class.

This leads to what is a responsibility in software design? There is no trivial answer, this is why Robert C. Martin (Uncle Bob) rewrote the SRP principle this way:

A class should have one reason to change.

This leads to what is a reason to change?

SRP is a principle that cannot be easily inferred from its definition. Moreover, the SRP leaves a lot of room for its own opinions and interpretations. So what is SRP about? SRP is about logic partitioning into code: which logic should be declared in which class. 

The goal of this post is to propose objective and concrete guidelines to increase your classes’ compliance with the SRP. In-fine, this will increase the maintainability of your code.

SOLID Principles Summary

SOLID Principles - Single Responsibility Principle

Before delving into the Single Responsibility Principle, let’s take a quick look at how it stands in the SOLID design principles:

  • The Single Responsibility Principle (SRP): A class should have one reason to change. This principle is about how to partition your logic into classes and avoid ending up with some monster classes (known as god class).
  • The Open-Close Principle (OCP): Modules should be open for extension and closed for modification. To implement a new feature, it is better to add a new derived class instead of having to modify some existing code.
  • The Liskov Substitution Principle (LSP): Methods that use references to base classes must be able to use objects of derived classes without knowing it. Array implements IList<T> but throws NotSupportException on IList<T>.Add(). This is a clear LSP violation.
  • The Interface Segregation Principle (ISP): The client should not depend by design on methods it does not use. This is why designers introduced interfaces like IReadOnlyCollection<T>. Often the client just needs a subset of features like read-only access instead of full read-write access.
  • The Dependency Inversion Principle (DIP): Depend on abstractions, not on implementations. Interfaces are much less subject to changes than classes, especially the ones that abide by the (ISP) and the (LSP).

Something to keep in mind is that SRP is the only SOLID principle not related to the usage of abstraction and polymorphism.

Single Responsibility Principle Explained with a C# Example

Let’s use the ActiveRecord pattern to explain a typical SRP violation and how to improve it. An ActiveRecord is a class with two responsibilities well-identified:

  • First, an ActiveRecord object stores in-memory data retrieved from a relational database.
  • Second, the record is active. It is active in the sense that it mirrors data in memory and data in the relational database. For that, the CRUD (Create Read Update Delete) operations are implemented by the ActiveRecord.

To make things concrete an ActiveRecord class can look like this:

If the class Employee didn’t know about persistence it would be a Plain Old CLR Object, what we call a POCO class. A class is POCO if it depends only on C# primitive types (int, string, bool…) and on other POCO types. Being a POCO class is a good start to abide by the SRP principle. This way we can avoid the usage of concerns like Databases, User Interfaces, Networks, or Threading.

Consequently, if the Employee class is a POCO, it implies that a dedicated module manages the persistence.

Benefits of Applying the Single Responsibility Principle

Here are the benefits:

  • Not all client code of the class Employee wants to deal with persistence.
  • More importantly, an Employee consumer really needs to know when an expensive DB roundtrip is triggered. If the Employee class is responsible for the persistence who knows if the data is persisted as soon as a setter is invoked?

Hence better isolate the persistence layer accesses and make them more explicit. This is why at NDepend we promote rules like UI layer shouldn’t use directly DB types that can be easily adapted to enforce any sort of code isolation.

Single Responsibility Principle and cross-cutting concerns

Persistence is what we call a cross-cutting concern, an aspect of the implementation that tends to spawn all over the code. We can expect that most domain objects are concerned with persistence. Other cross-cutting concerns we want to separate domain objects from include validation, log, authentication, error handling, threading, network, user interface and caching. The need to separate domain entities from those cross-cutting concerns is achievable through OOP patterns like the pattern decorator for example. Alternatively, some Object-Relational Mapping (ORM) frameworks and some Aspect-Oriented-Programming (AOP) frameworks can be used.

Single Responsibility Principle and Reason to Change

Let’s consider this version of Employee:

The ComputePay() behavior is under the responsibility of the finance people and the ReportHours() behavior is under the responsibility of the operational people. Hence if a financial person needs a change to be implemented in ComputePay() we can assume this change won’t affect the ReportHours() method. Thus according to the version of SRP that states “a class should have one reason to change”, it is wise to declare these methods in different dedicated modules. As a consequence, a change in ComputePay() has no risk of affecting the behavior of ReportHours() and vice-versa. In other words, we want these two parts of the code to be independent because they will evolve independently.

This is why Robert C. Martin wrote that SRP is about people : make sure that logic controlled by different people is implemented in different modules.

Single Responsibility Principle and High-Cohesion

The SRP is about encapsulating logic and data in a class because they fit well together. Fit well means that the class is cohesive in the sense that most methods use most fields. Actually, one can measure the cohesion of a class with the Lack of Cohesion Of Methods (LCOM) metric. See below an explanation of LCOM (extracted from this great Stuart Celarier placemat) What matters is to understand that if all methods of a class are using all instances fields, the class is considered utterly cohesive and has the best LCOM score, which is 0 or close to 0.

Typically the effect of an SRP violation is to partition the methods and fields of a class into groups with few connections. The fields needed to compute the pay of an employee are not the same as the fields needed to report pending work. This is why we can estimate the adherence to SRP and take action based on the LCOM metric. One can use the rule Avoid types with poor cohesion to track classes with poor cohesion between methods and fields.

Single Responsibility Principle and Fat Code Smells

We can hardly find an easy definition for what is a responsibility. However, we noticed that adhering to SRP usually results in classes with a good LCOM metric score.

On the other hand, not adhering to SRP usually leads to the God class phenomenon. A god class knows too much and does too much. Such god class is usually too large. Violations of rules like Avoid types too big, Avoid types with too many methods, Avoid types with too many fields are good candidates to spot god classes. Once god class are well identified, they can be refactored into a finer-grained design.

Simple Guidelines to Adhere to the Single Responsibility Principle

Here are objective and concrete guidelines to adhere to SRP:

  • Domain classes must be isolated from Cross-Cutting Concerns: code responsible for persistence, validation, log, authentication, error handling, threading, caching…
  • When implementing your domain, favor POCO classes that do not have any dependency on an external framework. A POCO class is not necessarily a fields and properties-only class but can implement logic/behavior related to its data.
  • Use your understanding of the Domain to partition code: logic related to different business functions should be kept separated to avoid interference.
  • Regularly check the Lack of Cohesion Of Methods (LCOM) score of your classes.
  • Regularly check for too large and too complex classes.

Conclusion

In conclusion, the Single Responsibility Principle (SRP) stands as a cornerstone in the foundation of SOLID design principles, particularly in the realm of C#. By adhering to SRP, developers can achieve code that is not only modular and maintainable but also resilient to change. Embracing the concept of a class having only one reason to change fosters a codebase that is easier to comprehend, test, and extend.

Let SRP be the compass that directs your coding endeavors. It will lead you to a destination of cleaner, more efficient, and ultimately more elegant software design.

Comments:

Comments are closed.