torsdag den 7. juni 2018

SOLID principles, in layman's terms: Interface Segregation

Raison d'être: I set out to write about the SOLID software development principles. Specifically, my aim was/is to make these things more understandable to other developers who, much in the way as yours truly, found it troublesome to have to decipher lengthy, complex articles and books on the matter. These principles are for everyone to learn and use, but I found that they were hard to fully grasp; quite possibly because I come from a non-English speaking background. So with this series of articles I'm setting out to try and de-mystify the principles, with only the best intentions in mind. The principles apply to many layers of software development. In my articles, I specifially aim to describe them as they relate to programming. I hope it'll be of use to you. Thank you for stopping by.

This will be a 5 article-series about SOLID. SOLID is all the rage; at least as far as the job-adds I'm reading are concerned; "you are expected to honor the SOLID principles" etc. So what exactly is SOLID about? Plain and simple, it's a set of guide-lines in developing object-oriented systems. They are a group of concepts that have proven themselves valuable for a great many people coding a great many pieces of software. A tale told by your elders in software engineering, if you will, that you will want to pay heed to so you can boast on your CV that you're into the SOLID principles - and you'll be a better developer for knowing them, I promise you that. Heck, you're a better developer for simply _wanting_ to know them!

SOL[I]D - The Interface Segregation principle

Round 4 of our series of articles on the SOLID principles brings us to the Interface Segregation. And once more we turn to wikipedia for an official explanation, where it states that, [quote] "that no client should be forced to depend on methods it does not use.[[/quote]. Let's get down and dirty - what's it all about, then?

It's very simple, really: Basically this ones means that if you're implementing interfaces and your implementation doesn't implement all methods of that interface, that's a sign that your interface is bloated and must might benefit from a redesign.

A brief example to the fore. Again, these are very simple examples, solely for the purpose of getting a grasp on the concept. Let's say we've got this interface, that we wish to implement, and two such implementations:

  public interface ILogLocation
    {
        string LogName { get; set; }
        void Log(string message);
        void ChangeLogLocation(string location);
    }

    public class DiskLogLocation : ILogLocation
    {
        public string LogName { get; set; }

        public DiskLogLocation(string _logName)
        {
            LogName = _logName;
        }

        public void Log(string message)
        {
            // do something to log to  a file here
        }

        public void ChangeLogLocation(string location)
        {
            // do something to change to a new log-file path here
        }

    }

    public class EventLogLocation : ILogLocation
    {
        public string LogName { get; set; }

        public EventLogLocation()
        {
              LogName = "WindowsEventLog";
        }

        public void Log(string message)
        {
            // do something to log to the event-viewer here
        }


        public void ChangeLogLocation(string location)
        {
            // we can't change the location of the windows event log, 
            // so we'll simply return
            return;
        }
    }

Above are two implementations of the ILogLocation interface. Both implement the ChangeLogLocation() method, but only our DiskLogLocation-implementation brings something meaningful to the table. This it messy; it's like putting a winter beanie over your summer cap, they're both hats but you needn't wear them both at the same time. Let's say that we have three DiskLogLocations and one EventLogLocation: now, for configuration purposes we may run through them all by adressing them by their interface, that would be a perfectly reasonable thing to do...

var logLocations = new List<ILogLocation>() {
                new DiskLogLocation("DiskWriteLog"),
                new DiskLogLocation("DiskReadLog"),
                new EventLogLocation(),
                new DiskLogLocation("FileCheckLog")
            };

            foreach( var logLocation in logLocations)
            {
                logLocation.ChangeLogLocation(@"c:\" + logLocation.LogName + ".txt");
            }

... so that's what we do in the above, but that leads as you can see to an unncessary call to the ChangeLogLocation() for the EventLogLocation-object. It's unnecessary, it doesn't lead to anything rather the potential of introducing bugs and is to be considered a code smell, and should be generally 'uncalled for' - parden the pun.

So here's what we can do instead:

public interface ILogLocation
{
    string LogName { get; set; }
    void Log(string message);
}

public interface ILogLocationChanger
{
    void ChangeLogLocation(string location);
}

public class DiskLogLocation : ILogLocation,  ILogLocationChanger
{
    public string LogName { get; set; }

    public DiskLogLocation(string _logName)
    {
        LogName = _logName;
    }

    public void Log(string message)
    {
        // do something to log to  a file here
    }

    public void ChangeLogLocation(string location)
    {
        // do something to change to a new log-file path here
    }
}

public class EventLogLocation : ILogLocation
{
    public string LogName { get; set; }

    public EventLogLocation()
    {
        LogName = "WindowsEventLog";
    }

    public void Log(string message)
    {
        // do something to log to the event-viewer here
    }
}

Above we've moved the ChangeLogLocation()-method into its own interface, ILogLocationChanger - which is implemented only the DiskLogLocation implementation. That's a much nicer way of going about it; no unneccesary method implementations ever again! Also, we now have a way of distinguishing the implementations, by way of the abstractions they implement. We could potentially re-use one or more interfaces for other classes.

So there you have it - the Interface Segregation principle, which basically means 'split up your interfaces if you find that you have to implement methods that aren't required for your class, or that you simply return, or throw an exception, from'. Easy!

As an aside, you might ponder the different from this Interface Segregation principle and the Single Responsibility principle; as a reminder, the Single Responsibility principle informs us that a module should carry a single responsibility only. Isn't that about the same thing as this Interface Segregation principle we've just taken on? Well, they're somewhat similar. There's a difference, but I won't go into it. I'll instead refer to StackOverflow user Andreas Hallberg, who I thought penned an explanation superior to anything I could've come up with: (ref: http://stackoverflow.com/questions/8099010/is-interface-segregation-principle-only-a-substitue-for-single-responsibility-pr) [quote]"Take the example of a class whose responsibility is persisting data on e.g. the harddrive. Splitting the class into a read- and a write part would not make practical sense. But some clients should only use the class to read data, some clients only to write data, and some to do both. Applying ISP here with three different interfaces would be a nice solution."[/quote] Great stuff! So we gather that the SR principle is about 'one thing, and one thing only' vs. the IS principle is about 'what we need, and only what we need'. Related principles, both valid, both important.


Hope this helps you in your further career. And good luck with it, too!

Buy me a coffeeBuy me a coffee

Ingen kommentarer:

Send en kommentar