Pragmatic SOLID – Part 4 – The Interface Segregation Principle

Clients should not be forced to depend on methods they do not use.

TL;DR

  • Do not let client code use large classes which expose them to risk.
  • Split these classes into smaller classes (Single Responsibility Principle), or Interfaces.
  • Keep class interaction lean and focused.
  • Let interface design be dictated by the consuming clients, or by a logical service a subset of methods provides (e.g. Logging, UIConfirmation).
  • Interface Segregation does not necessarily mean using the Interface keyword, but it is designed for the purpose!
  • Automatically creating an Interface every time you create a class or method will lead to trouble.
  • An interface can also be too big – and may require segregating further.

What’s the business value?

By avoiding a monolithic object (or interface) from which developers pick and choose arbitrary methods, code is easier to decouple, test and maintain. There will generally be fewer (unnecessary) dependencies and less code to review when making a change to a smaller interface.  However, over-application of the principle can create a large amount of code complexity through over-simplification for no real gain; e.g. creating a low level  interface with just one method which is only consumed by one class.

Come again?

This principle ensures cleaner code and  prevents creation of one class to rule them all, which is typically knitted into the entire code-base. This scenario often occurs when a primary central object exists, and over time, developers add single methods to it in a hurry. They typically add their function, show some working software and walk away. Left unchecked, they or other developers follow suit. Soon there’s 10 new methods on a class, polluting its very fabric.

Any seasoned developer knows that code is read and maintained far many more times than written, so it is a fundamental principle to make sure we optimize for the reader and produce a good architecture that doesn’t leave a new starter scratching their head. Having to scroll through endless large function lists makes it harder to maintain. It is also no longer clear what the primary purpose of the class is, and developers will be less keen to refactor the class as it becomes larger and larger. If all client classes take this monolithic beast as their constructor argument, it is never clear what aspect they really depend on.

By applying interface segregation and only providing client classes with  the interfaces they need, developers can

  • introduce less risk when changing the code
  • easily plug in different implementations and have lean abstractions
  • easily provide fake implementations for testing

Example

You can use classes to provide interface segregation through the façade pattern – much like applying the single responsibility principle. However, the more typical way is to create an Interface, and declare that your class inherits all interfaces your class provides. In C# multiple inheritance of classes is not supported, but multiple inheritance of interfaces is. One could also not use inheritance at all, and provide properties to get to interfaces, but you would lose the ability to cast objects to their interfaces.

Lets take the final example in our Single Responsibility Principle tutorial and see how we can make sure that we do not expose a PrintAGraphInA4 method to more than it needs to know. I’ve stripped out the dependent classes and changed the Main() routine so that it simply tries to call a helper method to Print the graph.
Here’s the code before ISP.

   namespace ISP
   {
        class Printer
        {
            public enum PaperType { A4_PAPER, A3_PAPER };
            public PaperType Paper { get; set; }
        }

        static class Program
        {
            // small, fictional program to load up a previous graph settings file
            // and print the graph
            static void Main()
            {
                MyGraph graph = new MyGraph();

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

                PrintAGraphInA4(graph);
            }

            static void PrintAGraphInA4(MyGraph graph)
            {
                // Note - the code only needed access to the Print Method
                // but was exposed to Load/Save/MathModel/GraphTitle
                Printer printer = new Printer();
                printer.Paper = Printer.PaperType.A4_PAPER;
                graph.Print(printer);
            }

        }

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

        class MyGraph
        {
            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);
            }

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

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

In the example above, the function PrintAGraphInA4 is now tightly coupled to a MyGraph class. We could have ‘solved’ that by creating a base class of Graph with overridable Print methods. However, that still leaves the PrintAGraphInA4 function exposed to a number of methods it does not need (Load/Save and more). This means we have to review PrintAGraphInA4 when MyGraph is changed. Thus creating a base class isn’t the silver bullet.

The more useful thing to do would be to create an IPrintable interface. This would allow our function to be reused by more than just graphs. It also means that ONLY changes to the IPrintable interface or the implementation of the graph’s IPrintable will be the catalyst for a code review of PrintAGraphInA4.

Here is the minimal code change:

        static class Program
        {
            // small, fictional program to load up a previous graph settings file
            // and print the graph
            static void Main()
            {
                MyGraph graph = new MyGraph();

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

                // because inheritance is used for MyGraph : IPrintable
                // we can simply pass the graph to automatically cast to IPrintable
                PrintAnyThingPrintableInA4(graph); 
            }

            static void PrintAnyThingPrintableInA4(IPrintable printable)
            {
                // we do not care what we are printing. 
                Printer printer = new Printer();
                printer.Paper = Printer.PaperType.A4_PAPER;
                printable.Print(printer);
            }
        }

        interface IPrintable
        {
            void Print(Printer printerDevice);
        }

        class MyGraph : IPrintable
        {

We could create interfaces for IGraphStorage and IGraphSettings, but only if is beneficial.

What if I need to pass multiple interfaces to a class, should I create an aggregate?
Bob recommends not passing the aggregate interface object around if multiple interfaces are used in a client method or class. In nearly every case it would be preferable to pass the specific interfaces.

So, when should I join interfaces together?
If, for example, a class has been split into a number of interfaces to cover client usage patterns but two interfaces are always used together to deliver the full service to the client, it might make sense to combine those interfaces into one. This is especially true if both interfaces implement one or more methods in the same way and you believe there was an over zealous implementation of ISP.

Should I develop a service based model or a client based model?
A service model delivers an interface whose methods provide a distinct service to clients. We might develop such an interface in the absence of any client code. E.g. IPushNotificationService, with a simple SendNotifications() method.

A client model delivers an interface whose methods provide only what is required from empirical analysis of client usage patterns. If left unmonitored, this can introduce many client-specific interfaces tailored to specific needs of individual classes/methods. This can really help minimize the impact of change, but also can lead to inflexibility and prevent reuse.

The Middleton Rule

  • If clients of a class consistently only use a subsection of methods, it’s likely those methods should be an interface.
  • If you are passing a large object around because it’s easy – you are only storing up technical debt. Apply some form of ISP.
  • Don’t get silly. A gazillion “one method interfaces” will drive you crazy.
  • When you start to think “I shouldn’t change this class because so many others might use it and I don’t know the impact”, you’re probably exposing too much.
  • If you find fat interfaces are causing a huge dependency chain for your production code (and tests) , apply some more ISP.
  • For fat 3rd party interfaces, create your own Adapter in the middle and separate interfaces.

References

http://www.oodesign.com/interface-segregation-principle.html

http://www.objectmentor.com/resources/articles/isp.pdf

Advertisements

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