S.O.L.I.D. By Example: Single Responsibility Principal

I'll be presenting on the topic of Dependency Injection at the South Sound .NET Users Group on February 12th, 2009. In an effort to beef up my presentation to close to two hours, I decided to give a brief introduction to the S.O.L.I.D. Principals as described by Uncle Bob Martin as supporting material/introduction to Inversion of Control and Dependency Injection.

I sat down last night and started writing sample code that demonstrated each of the principals. The first being, the Single Responsibility Principal.

"A class should have one, and only one reason to change."

For example, let's take a look at the following User class:

namespace Sample.BigBallOfMud
{
    public class User
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public string GenerateUpdate()
        {
            return String.Format(
                "UPDATE Users SET FirstName='{0}', LastName='{1}' WHERE Id={2}",
                                 FirstName, LastName, Id);
        }

        public string GenerateDelete()
        {
            return String.Format(
                "DELETE FROM Users WHERE Id={0}", Id);
        }

        public string GenrateInsert()
        {
            if (Id != 0)
                throw new InvalidOperationException(
                    String.Format(
                        "This user already exists with an ID of {0}",
                        Id));

            return String.Format(
                "INSERT INTO Users VALUES ({0},{1})",
                FirstName, LastName);
        }

        public bool IsValid()
        {
            return !String.IsNullOrEmpty(FirstName) &&
                   !String.IsNullOrEmpty(LastName);
        }
    }
}

This class represents a user and various functions that related to the concept of user. In the context of SRP this class has three distinct responsibilities:

  1. Uniquely identify and represent a user in the system.
  2. Map the user to various SQL statements for interaction with the database.
  3. Validate that the user is valid based on some business specification.
    SRP tells us that each of these responsibilities should be in a separate class. Uncle Bob explains this principal best:

"Because each responsibility is an axis of change. When the requirements change, that
change will be manifest through a change in responsibility amongst the classes. If a class
assumes more than one responsibility, then there will be more than one reason for it to
change.

_
If a class has more then one responsibility, then the responsibilities become coupled.
Changes to one responsibility may impair or inhibit the class’ ability to meet the others.
This kind of coupling leads to fragile designs that break in unexpected ways when
changed."_

Given this principal let's consider the following refactoring of the user class:

namespace Sample.SOLID
{
    public class User
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class UserSQLMapper
    {
        private readonly User _user;

        public UserSQLMapper(User user)
        {
            _user = user;
        }

        public string GenerateUpdate()
        {
            return String.Format(
                "UPDATE Users SET FirstName='{0}', LastName='{1}' WHERE Id={2}",
                _user.FirstName, _user.LastName, _user.Id);
        }

        public string GenerateDelete()
        {
            return String.Format(
                "DELETE FROM Users WHERE Id={0}",
                _user.Id);
        }

        public string GenrateInsert()
        {
            if (_user.Id != 0)
                throw new InvalidOperationException(
                    String.Format("This user already exists with an ID of {0}",
                                  _user.Id));

            return String.Format(
                "INSERT INTO Users VALUES ({0},{1})",
                _user.FirstName, _user.LastName);
        }
    }

    public class ValidUserSpecification
    {
        public bool IsSatisifedBy(User user)
        {
            return !String.IsNullOrEmpty(user.FirstName) &&
                   !String.IsNullOrEmpty(user.LastName);
        }
    }
}

We now have three very specific class that are descriptive of what their responsibilities are. The User class has been decoupled from database concerns and validation concerns. It clearly represents a single concept and only needs to change when we redefine what a User is.

Database mapping concerns have been encapsulated in the UserSQLMapper class. It's sole responsibility is to map a user to various sql statements. The only reason for this class to chance is a Schema change in the database. At first glance you might say it has an additional change vector of the User class. But if we add additional properties to User, the UserSQLMapper only needs to care if we are mapping that property to the database. If we are mapping to the database, the schema has changed. So one responsibility there.

Validation of the User class has been broken out into a Specification class called ValidUserSpecification. It's responsibility is pretty clear. It answers the question, "Is this a valid user?". Specifications in this style are a nice way to do validation rules, but I'll save that for another post.

BONUS: One aspect of SRP that is not very clear from this example, is how it can lead to better design through refactoring.

One of the most common complaints about SRP is object explosion. If I am constantly breaking classes down into single responsibilities, won't I trade the complexity of large objects for the complexity of thousands of little objects?

This is a valid concern. But consider our example above. Imagine we now have 10 classes that have been broken down in this same Entity, Mapper, Specification way giving us 30. Would it not be possible to look at the set of 10 mapping classes and reduce them down to some smaller subset of classes that perform the responsibility in a generic fashion?

When things are broken down by concern like this it is far easier to consider the concern in isolation and come up with a better design for that single concern.

Follow me on Mastodon!