Monday, April 19, 2010

SRP and complexity

The Single Responsibility Principle (SRP) is probably the most important concept of good design.  But even once you know about it, and have read up on it, and seen countless blog articles describe and reference it, you may find yourself hesitant to actually follow it in real life.

The usual argument against it is that it seems like it might increase the complexity of your code.  Lets look at an example of applying SRP to a method.

public void UpdatePrimaryThingStatus( string status )
{
  Thing primaryThing = null;
  foreach( Thing t in something.AllThings )
  {
    if ( t.IsPrimary )
    {
      primaryThing = t;
      break;
    }
  }

  if ( primaryThing != null )
    primaryThing.Status = status;
}
There's nothing _wrong_ with this code, but it doesn't really follow SRP because the method is updating the primary thing's status, as advertised, but it's also finding the primary thing.  Lets factor out the finding of the primary thing into its own method:

public void UpdatePrimaryThingStatus( string status )
{
  var t = GetPrimaryThing();
  if ( t != null )
    t.Status = status;
}

public Thing GetPrimaryThing()
{
  foreach( Thing t in something.AllThings )
  {
    if ( t.IsPrimary )
      return t;
  }
  return null;
}
Notice how much code actually disappeared here.  And notice how simple each method is.  But, we did add a new method to the class.  Do we intended to reuse this method?  That depends, it IS a useful method that could easily be reused, but since we didn't have it already, lets assume we don't need to reuse it right now.  So yes, we simplified the code in the individual methods, but by adding a new method we've increased the complexity of the class.

We should probably ask why adding a new method is a problem.  It's only one new method!  It's well defined with a single responsibility, with an intention revealing interface, and simple code to boot.  Why would we think this is going to increase the complexity of the class?  Probably because we're used to working with classes that are thousands of lines long with lots and lots and lots of methods!  So yes, if you're applying SRP to your methods, but not to your classes, things might get a little complex.  But make sure your classes have a single responsibility, and you'll find that this wont be the case anymore.

OK, so if our classes are following SRP, then we'll be breaking large classes into more smaller classes.  But now we have lots of classes!  Doesn't that make our code more complex?

This same pattern will follow right up the chain through namespaces and assemblies...  is this getting out of control?  What's the solution?

The solution is cohesion!  You can add lots of small classes as long as they are all part of a cohesive whole.  This is actually a really beautiful thing.  If your classes are well organized, and obviously form a cohesive unit, you get an amazing benefit.  Lets say you need to go into the code and find a bug.  You know what area the bug is in, even if you don't know exactly what it is.  There may be 20 files that make up your code, but you'll probably only need to crack open 3 or 4 of them to find and fix the bug.  And each file you do open will be understandable, dare I say, easily understandable.

You may think there is still a problem in understanding the WHOLE.  To understand how it all works, don't you now need to open all these little classes and figure out how they all work together?  Yes and No.  Again, I think the fact that each class has a clear single responsibility (and therefore an intention revealing interface) means you can actually understand the WHOLE and read less code than if you had it all squished into a single class.

So, whatever you do, don't let fear of complexity drive you away from SRP.

3 comments:

  1. I totally agree, but possibly from a possibly different point of view.

    When I read your first example I too immediately saw the multiple-responsibilities but I wanted them separate so that I could test the two responsibilities separately. If we had one object/type whose only purpose was finding primary objects, we could test that independently. Then when testing this component, we could assume the finding of primary objects is working, and only concern ourselves with changing that object's status (leaving the obvious mutable objects and programming to side effects discussion for another day). The logical conclusion to this is a phylosophy that a number of developers I know have which states that the ideal line count of all methods should be 1, maybe 2 lines. I find this taking things a little too far, but most of the time can't come up with any real reason against it. The real trick is convincing your developers to take the time to write minimalistic clean and clear code which does exactly what the client wants and nothing more.

    Programming/testing this was does end up with lots of small classes which can make seeing the "forest" more difficult (this is especially true when your language doesn't support closures or first class functions so you end up with classes with no data and a single method which really is just a 'hack' around making private functions public but does have the small benefit of forcing developers to consider where an function should exist given the real data it is touching). But I find it much easier to understand and reuse work this way.

    To take the examples a step further, if we could encapsulate the searching of a list of objects by an isPrimary flag (using an interface) or even more abstractly an arbitrary boolean flag (probably need reflection at this point), then we could test that generic searching only once and reuse it throughout our application. That would leave the UpdatePrimaryThingStatus only needing to "know" it should search for the primary object and update its status but not need to know "how" to search.

    ReplyDelete
  2. I agree with you, but I don't think testing alone is a very strong argument. After all, not everyone is writing tests. So the argument that you should use small methods so you can test wont go over well with that crowd. That's why I think its important to focus on the fact that writing tests also encourages good design, and that good design is good in and of itself, even without tests.

    ReplyDelete
  3. "After all, not everyone is writing tests" - and the root problem comes out ;-)

    I agree.

    ReplyDelete