SOLID is a popular acronym that refers to a set of 5 important class design principles. I know developers get overloaded with the endless jargon that keep popping up, especially when it seems to do little more than complicate what seems pretty straightforward. Do know that we use jargon not only to make ourselves feel smart, but also as the basis for a common vocabulary which can have real benefits when trying to communicate complex systems.
Today I want to talk about the first part of SOLID: Single Responsibility Principle (SRP). This is a pattern that is simple to understand on paper, but not so clear-cut in practice. SRP states that a class should have one and only one responsibility. There are a number of benefits to following this principle:
- Code complexity is reduced by being more explicit and straightforward,
- Readability is greatly improved,
- Coupling is generally reduced,
- Your code has a better chance of cleanly evolving
It's quite logical – if a class focuses on one specific responsibility, it's bound to be cleaner and more receptive to change.
Great, super-clear on paper, but what does it all actually mean?
The first problem we need to address is: what is a responsibility. I think of it as a fancy word for purpose. That purpose can be whatever you need from your class. Many of your classes will form the basis of your domain model, and the single responsibility of such classes is to be to represent their respective real-world counterparts. The responsibility of our Car class is to represent a real-world Car. Not to worry about data access, presentation, or the purchasing aspect of our domain. Similarly, we might have a utility class for data access – such a class must avoid doing anything not directly related to data access.
Here's an example. Given a DataAccess class that looks something like:
public class DataAccess
{
public class GetUser(string userName, string password)
{
using (var connection = new SqlConnection(...))
using (var command = new SqlCommand())
{
command.CommandTex = "SELECT Id, Name, UserName, Password, DateOfBirth FROM Users where UserName = @UserName and Password = @Password";
command.Parameters.Add("@UserName", SqlDbType.VarChar).Value = userName;
command.Parameters.Add("@Password", SqlDbType.VarChar).Value = password;
command.Connection = connection;
connection.Open();
using (var reader = command.ExecuteReader())
{
return dr.Read() ? MapUser(dr) : null;
}
}
}
private User MapUser(IDataReader dr)
{
return new User
{
Id = Convert.ToInt32(dr["Id"]),
Name = dr["Name"].ToString(),
UserName = dr["UserName"].ToString(),
Password = dr["Password"].ToString(),
DateOfBirth = Convert.ToDateTime(dr["DateOfBirth"]),
};
}
}
We have a violation of our single responsibility principle – DataAccess has two clear and distinct purposes: data access and data mapping. Now, it's true that data access and data mapping go hand-in-hand, but the unnecessary coupling of logic within a single class makes both functions (access and mapping) unnecessary brittle. By being in the same class, GetUser is tightly coupled to the mapping logic (which makes it more likely to be negatively effected by any changes).
Another way to look at SRP is from the perspective of reason to change. Wikipedia gives a good illustration from this perspective:
As an example, consider a module that compiles and prints a report. Such a module can be changed for two reasons. First, the content of the report can change. Second, the format of the report can change. These two things change for very different causes; one substantive, and one cosmetic. The SRP says that these two aspects of the problem are really two separate responsibilities, and should therefore be in separate classes or modules. It would be a bad design to couple two things that change for different reasons at different times.
The reason it is important to keep a class focused on a single concern is that it makes the class more robust. Continuing with the foregoing example, if there is a change to the report compilation process, there is greater danger that the printing code will break if it is part of the same class.
The other problem I see people having when it comes to applying SRP is an irrational dislike for having multiple classes and a fear of object instantiation. At the very least, the solution to our above design problem is to create a new class, named DataMapper, and create an instance of it from within our DataAccess class. This would provide logical separation (which is what SRP is concerned about). However, we'd still have tight coupling and would likely consider introducing an IDataMapper interface and leverage dependency injection (other parts of SOLID focus on those aspects, so we'll skip the details for now). The point is that we'd introduce a new type (possibly two), and an extra object instantiation:
using (var reader = command.ExecuteReader())
{
return dr.Read() ? new DataMapper().MapUser(dr) : null;
}
//or
using (var reader = command.ExecuteReader())
{
return dr.Read() ? Factory.Create<IDataMapper>().MapUser(dr) : null;
}
I consider both side effects extremely insignificant (non-existent really) in comparison to what is gained (readability, maintainability and testability).
In the end, the lesson here is that you should put thought into each and every one of your classes. That starts by first defining a very specific purpose for each class – regardless or how many .cs files you end up with. But don't think that SRP is something you can apply at the start and naturally maintain. As your system grows so too will your classes. At some point you'll notice that a class that once had a defined purpose seems to be a little murky. Do not hesitate to immediately refactor your code.
Posted
12-05-2008 8:40 AM
by
karl