The Single Responsibility Principle

“It is a bad plan that admits of no modification.”
— Publilius Syrus, First Century BC

The Single Responsibility Principle states a class, module or function has one responsibility.

Ultimately, this is the definition of cohesion.

Often this principle is defined as a module (or class, or function) should have only one reason to change. It assumes code that must be changed for a myriad of reasons is usually arbitrarily coupled code. A clue you have code violating this principle is when changing an implementation of something in one class, you find you need to make code changes in several classes.

For an example, consider the following:

public class DessertProvider  : System.Configuration.Provider.ProviderBase
{
     private string _connectionString;
     public override void Initialize(string name, NameValueCollection config)
     {
         base.Initialize(name, config);
         _connectionString = config["connection"];
     }

     public bool CanHaveDessert(string name, string tonightsDessert)
     {
         bool result = false;
         using (SqlConnection cn = new SqlConnection(_connectionString))
         {
             cn.Open();
             SqlCommand cmd = new SqlCommand(QueryString, cn);
             string query = @"SELECT COUNT(*) FROM foods
                             INNER JOIN plan_foods on plan_foods.ID = foods.ID
                             INNER JOIN plans on plan_foods.Plan = plans.ID
                             INNER JOIN people on people.Plan = plans.ID
                             WHERE foods.Name = @1 and people.Name = @2";
             // ... elided ... execute the query to see if
             // tonightsDessert is in "names" diet plan
         }
         return result;
      }

}

This code runs in the app that tells an individual staff person whether a client gets dessert.

There are (at least) 3 unrelated reasons this code may have to change:

  1. The algorithm that decides whether someone gets dessert is changed.
  2. The way the source data is persisted.
  3. The algorithm that configures the code to fetch the diet plans (via the ProviderBase)

To understand the consequences of that design, consider a new requirement. Some of our customers discovered most of their clients have identical diet plans. To improve the dietician staff workload, they’d like to manage diet plans for groups of people instead of modifying each person’s individual plan.

Being the customer focused organization we are, we work up a new design for our next release. Now, instead of a diet plan belonging to a user, we subscribe multiple people to a diet plan.

Unfortunately, that change just broke the desert approval module. This lack of cohesion means that a design change to improve the life of dieticians has totally hosed how we determine whether anyone gets dessert (I’m pretty sure it messed up the Salad Module, too). What other distantly related code has that change broken?

So – we search, fix, release to QA. Search and fix the one’s we missed the first go-round. Test. Our stakeholders wonder why the change is taking so long. More searching and fixing.

Version 3 — the SOA release! Meal plans will be exposed via an SOA Service instead of a database. So, we must find all the code using the database, and change it to use an SOA enabled interface. Unfortunately, the compiler won’t help us – despite the change in architecture, the code is remains syntactically valid. More searching, fixing, testing, late nights and explaining to stakeholders. Here’s hoping for good test coverage.

Hmm — we also no longer need the database connection information coming in through the ProviderBase. It’s dead code. So, let’s rip it out. Another QA drop. Or are we so late in the cycle, we decide to leave the dead code in (we’ll get it in a service pack … riiiiight)?

So, why did the database schema need to be exposed to the DessertProvider? Why did it have to know about a database at all, when all it wanted was a meal plan for a person? And what is it about dessert determination that requires configuration via the .NET Provider Model? The answer to both of those questions is “nothing”. The original design insists it does, and then gets in the way of the inevitable changes.

Here’s an alternative approach that better separates responsibilities:

///
/// Supplies the algorithm to determine whether a patient gets
/// dessert.
///
public class DessertProvider
{
     private IMealPlanRepository _mealPlans;
     public DessertProvider(IMealPlanRepository mealPlanRepository)
     {
         _mealPlans = mealPlanRepository;
     }

     public bool CanHaveDessert(string name, string aDessert)
     {
         IMealPlan plan = _mealPlans.ForMember(name);
         return plan.Contains(aDessert);
     }
 }

///
/// Describes a protocol for getting at our MealPlans
///
public interface IMealPlanRepository {
     IMealPlan ForMember(string memberName);
}

///
/// Implement the IMealPlans interface over an SQL Server database
///
public class SqlMealPlanRepository : IMealPlanRepository
{
    public SqlMealPlanRepository(string connectionString)
    {
         // ...
    }
    public IMealPlan ForMember(string memberName)
    {
         // SQL query goodness here ...
    }
}

/// Some entity that knows how to supply our various services.
/// Consider an IoC container (such as Unity or StructureMap)
public class MyAppFactory
{
     // ...
     public MyAppFactory(NameValuePair configData)
     {
         ConfigData = configData;
     }

     private NameValuePair ConfigData
    {
       get;
       set;
    }

    public IMealPlans MealPlans
    {
        get
        {
             return new SqlMealPlanRepositor(
                 ConfigData["connectionstring"]);
         }
     }
}

Now – when the meal plan schema changes – the only code that needs to change is our SqlMealPlanRepository.

Nothing else. No hunt/fix/test/hunt/fix/test.

If we go SOA, the only code that needs to change is a new IMealPlanRepository implementation served up by our Factory.

That’s it.

If the algorithm to determine whether someone gets desert changes, we change our DessertProvider.

Period.

But wait – there’s more! As an added bonus – our code just became unit-testable (the original was testable in the large, but it wasn’t unit testable – it required a functioning SQL DB, with an appropriate schema, and loaded with data that the other tests can’t mess with). We can test the DessertProvider in isolation without having a database or SOA bus around.

And they thought giving you a flashlight with the Snuggie was a good deal …

Powered by WordPress with GimpStyle Theme design by Horacio Bella.
Entries and comments feeds. Valid XHTML and CSS.