Pragmatic SOLID Part 1 – The Single Responsibility Principle

There should never be more than one reason for a class to change. – Robert C. Martin February 1997

TL;DR

  • Break up your classes into teeny tiny ones.
  • You’re aiming to limit the impact and complexity of change within a class.
  • Don’t use brainlessly – there needs to be a symptom or good reason.

What’s the business value?

The ideal outcome of applying the Single Responsibility Principle (SRP) is to reduce assessment, development and testing time when making changes to a class in future. We like that, right? We could even use that in a discussion with Product Owners around reducing technical debt! Wait, it does more than that. The principle consequently effects an improved flexibility and reuse pattern through a class which is simpler, smaller and less prone to consequential defects. Each resulting class starts to become more cohesive and testable.

This is achieved by decomposing your (lovely) encapsulated class by separating concerns within the class, into smaller classes.

A class’ data, calculation capability, save/load capability and print capability result in four classes. You then have ultimate confidence what the impact of your change will be. Some would even go so far as to split save from load.

It’s not all good news though. Some people quote the SRP to say “this class does one thing and one thing only, really well”, I think that’s a misinterpretation. Applying the principle unnecessarily can quickly lead to increased complexity and confusion on large projects due to high volumes of small, poorly named single-method classes which would have previously been encapsulated within one class.  Good naming conventions, and feature grouping can help with this, but why are you now fighting with your code? Something went wrong.

Show me an example!

Here’s an example which demonstrates the SRP applied to a class which encapsulates a graph. The points are calculated based on a transformation model. The graph can persist and restore its settings on disk, and provides the capability to print given a Printer class.

namespace MyGraphProgramV1
{
    // dummy class
    class Printer
    {
    }

    static class Program
    {
        // small, fictional program to load up a previous graph settings file
        // print the old version, update the title and mathematical model
        // and resave the settings
        static void Main()
        {

            MyGraph graph = new MyGraph();

            graph.Load(@"C:\settings.txt");

            Printer printer = new Printer();
            graph.Print(printer);

            graph.GraphTitle = "NewTitle";
            graph.MathModel = TransformationModel.modelX2;

            graph.Save(@"C:\settings.txt");
        }
    }

    public enum TransformationModel { none, modelX1, modelX2, modelX3 };

    class MyGraph
    {
        // version 1 - multiple reasons to change
        // 1. If a setting changes
        // 2. if the printing method changes
        // 3. If the calculation engine changes
        // 4. If the storage mechanics change
        // 5. If the point array changes
        // the class as a whole is at risk when each of these change. 
        // imagine if each function was a few hundred lines. 
        // This would start to become difficult to read.

        public MyGraph() { ; }

        public String GraphTitle
        {
            set
            {
                graphTitle = value;
            }
            get
            {
                return graphTitle;
            }
        }

        public TransformationModel MathModel
        {
            get { return transformationModel; }
            set
            {
                transformationModel = value;
                CalculatePoints();
            }
        }

        public void Load(String fileName)
        {
            // load title and transformation model, probably from an xml file
            // possibly upgrade the settings in future.
            CalculatePoints();
        }

        public void Save(String fileName)
        {
            // save title and transformation model, handle file exceptions. 
        }

        public void Print(Printer printer)
        {
            // print title and points, draw some axis, make it look pretty.
            // this function could get quite large and use a lot of common classes.
        }

        private void CalculatePoints()
        {
            // depending on transformationModel, 
            // populate points with a mathematical algorithm
            // this will probably be a large switch statement to start with.
        }

        private int[,] points = new int[10, 10];
        private TransformationModel transformationModel;
        private String graphTitle;
    }
}

As you will see with the Single Responsibility Principle applied below, some classes still have more
than one reason for change. You can chase down the rabbit hole but that can get silly. You have to ask yourself where do you require reduced complexity, clarity, reuse, testability. The code below reduces the impact of a change on the entirety of the MyGraph class.

namespace MyGraphProgramV2
{
    // dummy class
    class Printer
    {
    }

    static class Program
    {
        // small, fictional program to load up a previous graph settings file
        // print the old version, update the title and mathematical model
        // and re-save the settings
        static void Main()
        {
            // version 2 Single Responsibility Principle implemented 
            // Note: there are no changes to our client!.
            MyGraph graph = new MyGraph();

            graph.Load(@"C:\settings.txt");

            Printer printer = new Printer();
            graph.Print(printer);

            graph.GraphTitle = "NewTitle";
            graph.MathModel = TransformationModel.modelX2;

            graph.Save(@"C:\settings.txt");
        }
    }

    public enum TransformationModel {none, modelX1, modelX2, modelX3};

    class MyGraph
    {
        // version 2 - Single responsibility principle applied
        // This class' responsibility is now to orchestrate and 
        // route data between SRP classes.
        // It's much easier to read. Reasons for change:
        //    1. If classes require more data or different interfaces
        //    2. If we add more properties we have to route them on to the settings.

        // We could create these classes when needed, most don't need to be members. #todo
        private MyGraphSettingsModel settings = new MyGraphSettingsModel();
        private MyGraphDataModel dataModel = new MyGraphDataModel();
        private MyGraphStorage storage = null;
        private MyGraphPrinter printer = null;
        private MyGraphPointsCalculator calc = null;

        public MyGraph()
        {
            storage = new MyGraphStorage(settings);
            printer = new MyGraphPrinter(dataModel);
            calc = new MyGraphPointsCalculator(dataModel);
        }

        public TransformationModel MathModel
        {
            get 
            { 
                return settings.Model; 
            }
            set
            {
                settings.Model = value;
                calc.CalculatePoints(settings.Model);
            }
        }

        public String GraphTitle 
        {
            get { return settings.GraphTitle; }
            set { settings.GraphTitle = value; }
        }

        public void Load(String fileName)
        {
            storage.Load(fileName);
            calc.CalculatePoints(settings.Model); // need to calculate points after a load
        }

        public void Save(String fileName)
        {
            storage.Save(fileName);
        }

        public void Print(Printer printerDevice)
        {
            printer.Print(printerDevice);
        }
    }

    class MyGraphSettingsModel
    {
        // This class' reasons for change: 
        // 1. If a property is added or removed from the settings class

        private TransformationModel model;
        private String graphTitle;

        public TransformationModel Model
        {
            get { return model; }
            set { model = value; }
        }

        public String GraphTitle
        {
            get { return graphTitle; }
            set { graphTitle = value; }
        }
    }

    class MyGraphDataModel
    {
        // This class' reasons for change: 
        //    1. If the data points model changes 

        public int[,] points = new int[10,10];
    }

    class MyGraphStorage
    {
        // This class' reasons for change:
        //  1. if the settings model changes 
        //     - this class could get busy if we need to perform 
        //       upgrades on older versions when loading.
        //     - by its own merit that's a great reason to move 
        //       it away from the main class
        //  2. If we change from file storage to internet/memory 
        //     - so we could separate further through interfaces.
        // 
       private MyGraphSettingsModel settings = null;

       public MyGraphStorage(MyGraphSettingsModel model)
       {
           settings = model;
       }

       public void Save(String fileName) { return; } // does nothing for now
       public void Load(String fileName) { return; } // does nothing for now
    }

    class MyGraphPrinter
    {
        // This class' reasons for change
        // 1. If the data model changes, we have to update the 
        //    printing routine - we could pass explicit properties instead
        // 2. if the printer API/interface changes
        private MyGraphDataModel dataModel = null;
        public MyGraphPrinter(MyGraphDataModel model)
        {
            dataModel = model;
        }
        public void Print(Printer printer) { }
    }

    class MyGraphPointsCalculator
    {
        // This class' reasons for change
        // 1. The implementation of the calculation engine changes
        // 2. a new calculation is required.
        MyGraphDataModel dataModel = null;
        public MyGraphPointsCalculator(MyGraphDataModel model) 
        { dataModel = model; }
        public void CalculatePoints(TransformationModel model) 
        { ; } // update points
    }
}

When should I retrospectively apply the principle to a class?

In Robert Martin’s Book (Agile principles, patterns and practices in C#), he warns against applying the Single Responsibility Principle to introduce needless complexity. Ask yourself if you have skim read over that line. At times, I certainly forget that!

The Middleton test – to maximise ROI:

  • If unexpected defects are regularly introduced when this class is altered, decouple concerns, apply the principle to the class.
  • If you are planning on duplicating part of the functionality of this class, apply the principle to that part.
  • If you require future flexibility for varying implementations (E.g. calculation engines, drawing routines, printing routines, saving to different media), then apply the principle in anticipation of this.
  • If none of the above are true, and you have more valuable things to be undertaking – do them instead.

When exercising the principle, your newly extracted classes are ripe candidates for a strong set of supporting tests. This is especially important if the extracted classes are still complex internally and you plan to reuse it.

As you can tell I’m not an advocate of hyper-proactive application of SRP. I’m not alone. My bold assertion is – that If you are confident that your class is not going to be subject to change, doesn’t exhibit a set of bugs which flip-flop regularly and its code offers no future re-use, then it probably isn’t cost effective to apply the SRP to it.

So what is the official description of the Single Responsibility Principle?

Robert C Martin (Uncle Bob). Feb 1997
“In the context of the Single Responsibility Principle (SRP) we define a responsibility to be “a reason for change.” If you can think of more than one motive for changing a class, then that class has more than one responsibility.”
“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”

The typical examples given across the web are:

  • a class that not only holds the document data, but saves and prints it too.
  • a rectangle (or other geometric) class that has an area calculation function and a drawing function.

References

Full description (Chapter Excerpt) from Robert C. Martin

Developer Express video on SRP

Wikipedia

Curly’s Law – Jeff Atwood

OODesign.com

Bob Martin explaining the SRP

And, just google. There’s lots of great examples and viewpoints.

(C) Giles Middleton  2013

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s