This article will change your perspective on working with the Single Responsibility Principle (SRP). The main point is to focus on finding balance when designing object-oriented systems and applying SRP.
Let’s start by explaining the most fundamental ideas to make this easier to understand.
SRP means that each module should have one reason to change. This means that those designing a module should understand who owns it and what type of changes might be needed. And remember, there should be only one reason for change!
Everything is clear: our goal is to break down one module into sub-modules and connect them using a single method (like message passing). The main benefits are preventing spread, keeping changes localized, and reducing the risk of breakage.
Our anti-SRP example represents one of the balances in Object-Oriented Design (OOD). In fact, there are many examples.
Example of Single Responsibility Obsession (Anti-SRP)
Using any principles in an obsessive way reminds us of a silver bullet, but since there is no silver bullet, obsession takes us in the opposite direction of the principle and can have very serious consequences.
For that reason, we should not approach it in an obsessive way; an obsession with SRP turns it into anti-SRP (it means it leads to many small classes/methods and spreads the logic between them.).
Let’s assume we have a data-driven application and need to communicate with database tables. In this case, it would be a repository. This means our context (which defines the work area and its boundaries) is a database service that fits all the functionality together and cannot be changed.
I will skip the CRC (Class-Responsibility-Collaboration) card and start by declaring functionalities to help demonstrate our context.
First of all, we will start with a demonstration of anti-SRP. The idea of the anti-single responsibility principle starts with obsessive use. Let’s start with an example code.
public class GetRepo<T, Guid> where T : class
{
private readonly DatabaseService _dbService;
public GetRepo(DatabaseService dbService)
{
_dbService = dbService;
}
public IEnumerable<T> GetAll()
{
return _dbService.ToList();
}
public T Get(Guid id)
{
if (id == Guid.Empty)
throw new Exception("Id can not be empty!");
return _dbService.SingleOrDefault(x => x.Id == id);
}
}
public class InsertRepo<T> where T : class
{
private readonly DatabaseService _dbService;
public InsertRepo(DatabaseService dbService)
{
_dbService = dbService;
}
public bool Insert(T instance)
{
if (default(T) == instance)
throw new Exception("Instance can not be empty!");
int rowEffected = _dbService.Insert(instance);
return rowEffected != 0;
}
}
public class UpdateRepo<T> where T : class
{
private readonly DatabaseService _dbService;
public UpdateRepo(DatabaseService dbService)
{
_dbService = dbService;
}
public bool Update(T instance)
{
if (default(T) == instance)
throw new Exception("Instance can not be empty!");
int rowEffected = _dbService.Update(instance, x => x.Id == instance.Id);
return rowEffected != 0;
}
}
public class DeleteRepo<T, Guid> where T : class
{
private readonly DatabaseService _dbService;
public DeleteRepo(DatabaseService dbService)
{
_dbService = dbService;
}
public bool Delete(Guid id)
{
if (id == Guid.Empty)
throw new Exception("Id can not be empty!");
int rowEffected = _dbService.Delete(x => x.Id == instance.Id);
return rowEffected != 0;
}
}
As you can see, we have a DatabaseService class that helps with communication. At the repo level, we have encapsulated classes (each method belonging to the repository is encapsulated in a single class).
Keep in mind that naming conventions can be crappy, but this is just one example!
What does this code show us?
The code above shows the same repositories, divided into CRUD functionalities, using DatabaseService. This example also demonstrates a low-cohesion approach with anti-SRP because overusing SRP leads to anti-SRP and low cohesion.
What is the reason for the code being anti-SRP? or how do we understand this is anti-SRP?
Well, as I mentioned above, the context tells us that “context is a database service that fits all the functionality together and cannot be changed.”. So, we understand that our context will provide us with a single database that will not change, and all operations will belong to this database.
Hmm, we see that our code breaks the localization principle. This means that when there is a change, it should happen in only one place or need only small adjustments.
The context defines change and dependency. So, in our case, when we want to change the repo logic, the changes affect each repo class (DatabaseService implementation is changed, then all repositories can be changed)!
Anti-SRP — overusing single responsibility brings us many problems;
- Low cohesion
- Violation localization principle
- A side effect of changing
- Complex maintainability
- Increase dependencies
Fixing Anti-SRP Problem With Object-Oriented Balance
Keep in mind that everything starts with a struggle with complexity. The main idea in SRP and in OOD is to overcome complexity.
So, if we can collect related functionalities for a single unit, it will help us to localize functionalities and increase cohesion. In this way, we balance the complexity.
Let’s create an example.
public class Repository<T, Guid>
{
private readonly DatabaseService _dbService;
public Repository(DatabaseService dbService)
{
_dbService = dbService;
}
public IEnumerable<T> GetAll()
{
return _dbService.ToList();
}
public T Get(Guid id)
{
if (id == Guid.Empty)
throw new Exception("Id can not be empty!");
return _dbService.SingleOrDefault(x => x.Id == id);
}
public bool Insert(T instance)
{
if (default(T) == instance)
throw new Exception("Instance can not be empty!");
int rowEffected = _dbService.Insert(instance);
return rowEffected != 0;
}
public bool Update(T instance)
{
if (default(T) == instance)
throw new Exception("Instance can not be empty!");
int rowEffected = _dbService.Update(instance, x => x.Id == instance.Id);
return rowEffected != 0;
}
public bool Delete(Guid id)
{
if (id == Guid.Empty)
throw new Exception("Id can not be empty!");
int rowEffected = _dbService.Delete(x => x.Id == instance.Id);
return rowEffected != 0;
}
}
As you can see, the code above shows that all functionalities are grouped together, and it does not have any bad smell. There is only one possible reason to change (code is fit for all) in this class, meaning the reason for change is consistent throughout.
The result is that when you apply SRP, do not be obsessive; be more flexible and apply it only when needed. Also, be careful with low cohesion.
Conclusion
Overusing the Single Responsibility Principle (SRP) can lead to problems in code structure. When SRP is applied too strictly, it often results in many small classes or methods, each handling only a tiny part of the functionality. This spreads the logic across multiple classes, making the code harder to understand and manage.
Use SRP wisely. Instead of splitting everything into separate classes, aim for high cohesion by grouping related functions together. This balance keeps code simple, focused, and easier to maintain.