torsdag den 7. juni 2018

SOLID principles, in layman's terms: Liskov Substitution

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!

SO[L]ID - The Liskov Substitution principle

The Liskov substitution principle is the third in the set of principles that make out the SOLID acronym. According to the wikipedia entry, it states that, [quote] "in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T) without altering any of the desirable properties of that program[/quote]. OK. Cool. But what the heck does that imply, exactly, and why is it important?

It implies that a derived class must be substitutable for its base class. That is to say, whatever methods you define in your base class, you must implement these in all your implementing classes (if you're programming to an interface) or derived classes (if you're programming to an abstraction). You can't, by way of the principle, choose to NOT implement a method, and you mustn't extend your implementation by offering functionality in the implementation that's not available in the base class.

Basically, it's a code of honor: You mustn't 'violate' the base class in your implementations, rather honor it and fulfill its every method.

Alright - so that's the principle. But why is it important? Well, it's important because we would like to always be able to refer to the base class or the interface as opposed to deal with implementations of a base class directly, because this leads us to greater de-coupling in our design, which in turn provides us flexibility, a blessing in systems development (all things being equal!). But if we can't trust our implementations to deliever the same result - albeit in their own unique ways - of our abstraction or interface, that's when we begin to throw ugly if-else lines around. And that's when code becomes bloated, recompilations abound... Ragnarök!

I'll demonstrate by way of example. Please consider the below:

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

public class DiskLogLocation : ILogLocation
{
    public DiskLogLocation()
    {
    }

    public bool LogVerbose { get; set; }

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

        if (LogVerbose)
        {
            // do someting to log a heck of lot more log-text here
        }
        else
        {
            // do someting to log not very much log-text here
        }
    }
}

public class EventLogLocation : ILogLocation
{
    public EventLogLocation()
    {
    }

    public bool LogVerbose { get; set; }

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

        // notice we don't check the LogVerbose bool here
    }
}

So here we have an interface, ILogLocation, with two different implementations. But only one of the implementations makes use of the LogVerbose boolean. That's our danger-sign: this is a behaviour that alters the design-logic of our interface, which explicitly specifies that boolean and designates it a property, with getter and setter methods, for all intents and purposes because it's meant to be used. But the EventLogLocation-class 'dishonors' the interface in as much it doesn't make use of it all. Wonder if that will come back and haunt us? Let's see below how the implementations might be instantiated:


ConfigurationSettings.AppSettings.type_of_log_location = "eventLog";

private ILogLocation logLocation;

static void Main(string[] args)
{
    string logLocationType = ConfigurationSettings.AppSettings.Get("type_of_log_location");

    switch( logLocationType )
    {
        case "disk":
            logLocation = new DiskLogLocation();
            break;
        case "eventLog":
            logLocation = new EventLogLocation();
            break;
    }

    logLocation.LogVerbose = true;
    logLocation.Log("Hoping to get a lot more text logged");
}

Seems we could land in hot water here; in the above, as we instantiate an ILogLocation implementation by way of a configuration-file. We're setting the LogVerbose property and thus would expect to log a lengthy text with lots of debug hints as Log()-method. But as our configuration file finds us instantiating an EventLogLocation() that's never going to happen!

The Liskov Substitution principle is great for keeping us out of trouble down the road. We'll share our code with others, and they needn't have to know about potential pitfalls like "oh, yeah, there's this thing you need to know about the EventLogLocation's Log()-method...". More importantly, it's a reminder to focus on the code-quality of our implementations, and re-design our interfaces accordingly if we should feel the need to extend or degrade our base-class implementations. That danger lies particularly in altering the behaviour of our base-class, which may for example necessitate ugly type-checks such as the below:

if ( logLocation is EventLogLocation)
{
    // we, the deveoper, know there's no check on the 'LogVerbose' bool
    // on the EventLogLocation implementation
    logLocation.Log("Hoping not so much to get a verbose log");
}
else
{
    logLocation.LogVerbose = true;
    logLocation.Log("Hoping to get a verbose log");
}

In the above we're doing a type-check on the logLocation, and reacting one way if it's this time and another if it's not. Not so good - we've gone beyond having to deal with only a base class or interface representation, and set ourselves up for maintenance problems down the road.

So that's basically what the Liskov Substitution principle holds: Honor the contract as set up by your base class or interface. Don't extend the implementations to hold logic that's not called for, don't skip implementing methods by simple calling a 'return()' or throw an exception from them. If you feel that urge, look into re-designing your 'contract' with the base class or interface instead.

When I was first introduced to it, the Liskov Substitution principle seemed to me to hold little value. "Who does this",  I thought, "who creates a base-class with methods and choose then NOT to implement them - what are they even for, then?" As it turns out, lots of coders. Myself included. It slowly creeps up on you, as various factors such as "you need to finish this project NOW!" or "it'll just be this one time I'm splitting my logic based on this type-check" affect the way you work, and code. But let me assure you it pays dividends down the road, paying attention to it - never more so when you're part of a bigger team, and must be able to trust your colleagues' code, and they yours.

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