Sunday, May 10, 2015

SOLID – Single Responsibility Principal

“Every class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.”

Our Goal

We have a few major goals when writing software:
  1. Easily maintainable and modified: Software should be easily worked on by competent software engineers.
  2. Understandable: A competent software engineer should be able to grok your code with little effort.
  3. Work Correctly

Followed well, point’s 1 and 2 lead to 3 being true decades from now. Even if you’re indifferent to fellow employees’ struggles with terrible code, looking at your own poorly structured code from 6 months ago can be its own an arduous task.

Classes

This is where the Single Responsibility Principal comes in. We want to create small classes that do one thing well. This lightens the cognitive load when trying to understand how parts of a system work together.

Let’s take a look at an example:

public class ClaimsHandler
{
    public void AddClaim(Claim claim)
    {
        if (claim.UserId == null)
            throw new Exception("userId is invalid.");
        if (claim.DateOfIncident > DateTime.Now)
            throw new Exception("This feels like a scam.");
 
        using (var sqlConnection = new SqlConnection(connectionString))
        using (var sqlCommand =
            new SqlCommand("dbo.CreateClaim", sqlConnection)
            { 
                CommandType = CommandType.StoredProcedure 
            }) {

            sqlCommand.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier)
                .Value = claim.UserId;
            sqlCommand.Parameters.Add("@DateOfIncident", SqlDbType.DateTime2)
                .Value = claim.DateOfIncident;

            sqlConnection.Open();
            sqlCommand.ExecuteNonQuery();
            sqlConnection.Close();
        }
    }
}

Our ClaimsHandler class is responsible for validating a claim and saving the data. Even though this example is only 28 lines long its needs to be broken up. Specifically, the validation and save logic need to go into their own classes. Why? The validation and save logic will change and become more complex as time goes on.

Let’s forward the clock a year:

public class ClaimsHandler
{
    public void AddClaim(Claim claim)
    {
        if (claim.UserId == null)
            throw new Exception("userId is invalid.");
        if (claim.Amount <= 0)
            throw new Exception("amount must be greater than $0.");
        if (claim.DateOfIncident > DateTime.Now)
            throw new Exception("This feels like a scam.");
        if (claim.User.Status == "Regular" &&
            claim.Amount > 3500)
            throw new Exception(
                "Regular accounts are not allowed to claim over $3500.");
        if (claim.User.MaxClaimAllowed < claim.Amount)
            throw new Exception(string.Format("User max claim is {0}.",
                claim.User.MaxClaimAllowed));
        if (claim.ClaimType == "Rental" && claim.User.Status != "Gold")
            throw new Exception(
                "Rental claims are only available for Gold members.");
 
        using (var sqlConnection = new SqlConnection(connectionString))
        using (var sqlCommand =
            new SqlCommand("dbo.CreateClaim", sqlConnection)
            { 
                CommandType = CommandType.StoredProcedure 
            }) {

            sqlCommand.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier)
                .Value = claim.UserId;
            sqlCommand.Parameters.Add("@DateOfIncident", SqlDbType.DateTime2)
                .Value = claim.DateOfIncident;
            sqlCommand.Parameters.Add("@Amount", SqlDbType.Money)
                .Value = claim.Amount;
            sqlCommand.Parameters.Add("@ClaimType", SqlDbType.NVarChar)
               .Value = claim.ClaimType;

            sqlConnection.Open();
            sqlCommand.ExecuteNonQuery();
            sqlConnection.Close();
        }
    }
}

Both the amount of validation and number of items sent to the stored procedure increased. This class has also become more difficult to read.

What if the save or validation logic is used somewhere else? Keeping the changes consistent across all of the variations is difficult.

Let’s break this class up:

public class ClaimsHandler
{
    private readonly ClaimsRepository _claimsRepository =
        new ClaimsRepository();
    private readonly ClaimsValidation _claimsValidation =
        new ClaimsValidation();
 
    public void AddClaim(Claim claim)
    {
        _claimsValidation.Validate(claim);
        _claimsRepository.SaveClaim(claim);
    }
}
 
public class ClaimsValidation
{
    public void Validate(Claim claim)
    {
        if (claim.UserId == null)
            throw new Exception("userId is invalid.");
        if (claim.Amount <= 0)
            throw new Exception("amount must be greater than $0.");
        if (claim.DateOfIncident > DateTime.Now)
            throw new Exception("This feels like a scam.");
        if (claim.User.Status == "Regular" &&
            claim.Amount > 3500)
            throw new Exception(
                "Regular accounts are not allowed to claim over $3500.");
        if (claim.User.MaxClaimAllowed < claim.Amount)
            throw new Exception(string.Format("User max claim is {0}.",
                claim.User.MaxClaimAllowed));
        if (claim.ClaimType == "Rental" && claim.User.Status != "Gold")
            throw new Exception(
                "Rental claims are only available for Gold members.");
    }
}

public class ClaimsRepository
{
    public void SaveClaim(Claim claim)
    {
        using (var sqlConnection = new SqlConnection(connectionString))
        using (var sqlCommand =
            new SqlCommand("dbo.CreateClaim", sqlConnection)
            { 
                CommandType = CommandType.StoredProcedure 
            }) {

            sqlCommand.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier)
                .Value = claim.UserId;
            sqlCommand.Parameters.Add("@DateOfIncident", SqlDbType.DateTime2)
                .Value = claim.DateOfIncident;
            sqlCommand.Parameters.Add("@Amount", SqlDbType.Money)
                .Value = claim.Amount;
            sqlCommand.Parameters.Add("@ClaimType", SqlDbType.NVarChar)
                .Value = claim.ClaimType;

            sqlConnection.Open();
            sqlCommand.ExecuteNonQuery();
            sqlConnection.Close();
        }
    }
}

We are now free to modify ClaimsValidation, ClaimsRepository and ClaimsHandler separately. If either the validation or the save logic changes we don’t have to modify ClaimsHandler.

Your class should only have one reason to change.

Systems

The Single Responsibility Principal applies not only to your classes but extends down into your methods, and variables. More importantly, it applies to individual systems and the whole company.

Let’s say we have a Calculate Service (one application) that contacts some other services and calculates some data before returning a result.


Our Calculate Service is already doing too much. It’s responsible for:

  • Taking input from the Client Service
  • Requesting data from Service A
  • Requesting data from Service B
  • Calculating a result
  • Returning the result to the Client Service


Our application is brittle because it's doing too much it’s. Making a change to one responsibility has the potential to impact all other responsibilities. What happens when Data Service A’s API changes? What about when the calculations needed change? Each one of the responsibilities listed above should be their own application.

Let’s break this up:


This system looks more complex but all of this complexity was already present in the single Calculator Service. Moreover, it comes with the benefit of long term maintainability. If any responsibility changes we only need to make changes to that application and push it alone.

Another benefit of this new system is the ability to scale well. Before breaking up the system, if we needed to process more calculations we needed to push the whole application to more servers. However, if our Calculator Application is the bottleneck we can push just this one application to another server.

The Single Responsibility Principal is the most fundamental and important of the SOLID Principals. Apply it liberally, everywhere.