Solid Series: Single Responsibility in C#
“Every module, class or function in a computer program should have responsibility over a single part of that program’s functionality, which it should encapsulate. All of that module, class or function’s services should be narrowly aligned with that responsibility.”
Or
“A class should have only one reason to change”
What is SOLID?
This is a series on the basics of the SOLID principles of software engineering. The SOLID principles were created by Robert C. Martin “Uncle Bob” who is a software engineer public speaker and author.
SOLID is an acrostic that stands for:
- Single Responsibility
- Open Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
There will be an article for each. Lets take a look at single responsibility.
Single Responsibility
Non Compliant Example
Consider this class that checks for duplicates:
class CheckForDuplicateOrder
{
public bool HasDuplicate(string id)
{
var client = new DocumentClient(new Uri("Endpoint"), "Authkey");
var results = client.CreateDocumentQuery<Document>(
UriFactory.CreateDocumentCollectionUri("DataBase", "Collection"),
$"SELECT * FROM c WHERE c.partitionKey = '{id}'",
new FeedOptions
{
PartitionKey = new PartitionKey(id),
});
var resultCount = results.Count();
return resultCount > 0;
}
}
You may think that it is doing one thing, checking the database for duplicate orders, but under the covers it is really doing several things.
-
Creating a document client
-
Creating and executing a query
-
Comparing the results.
What is wrong with this? This function and class have to ‘know’ about the database, and all the implementation details about the database. You could put the connection strings in a config but still the function is dependent on the particulars of the infrastructure you’ve chosen.
From the application level the logic here doesn’t care if I am using cosmos or SQL server or MySql, it just needs to know that it needs to look up record by id in the data store and see if it already exists or not.
It would be better if the database handling was done elsewhere and this class focused on the logic of whether or not the order is a duplicate.
Compliant Example
interface IOrderRepository
{
object GetByExternalIdentifier(string id);
}
class CheckForDuplicateOrderSolid
{
IOrderRepository _repo;
public CheckForDuplicateOrderSolid(IOrderRepository repo)
{
_repo = repo;
}
public bool HasDuplicate(string id)
{
var results = _repo.GetByExternalIdentifier(id);
return result.Count() > 0;
}
}
This example is using a repository pattern, who’s implementation details are not shown. This repo is used by the CheckForDuplicateOrder class, passed into the constructor. This gives this class just a single responsibility.
Benefits in this example:
- The code is much easier to unit test. Mock the repo and then test the logic independent of the database.
- Use of dependency injection (discussed more later).
- There is only one reason to change this class.
Conclusion
Here is a bit of Robert Martin’s recent take on the validity of this principle in 2020:
It is hard to imagine that this principle is not relevant in software. We do not mix business rules with GUI code. We do not mix SQL queries with communications protocols. We keep code that is changed for different reasons separate so that changes to one part to not break other parts. We make sure that modules that change for different reasons do not have dependencies that tangle them.
I completely agree. The single responsibility principal is almost an axiomatic software design principal at this point.