According to Wikipedia, the Liskov Substitution Principle states that “Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program”.  This means that structurally our derivations should not alter the shape of the base type in a way that could break the correctness of consumers, and behaviorally subtypes must not violate reasonable assumptions about the base type.  The rule is meant to help guide us in how we use inheritance when we design our object hierarchies.  Ten years ago I thought about inheritance primarily as a method to reuse code.  Code reuse is certainly one of the benefits of inheritance but when I think back to some of the hierarchies I designed without considering LSP I would suggest that using inheritance at all was more trouble than it was worth.  The idea with Liskov Substitution Principle is that if we follow this principle, consumers of the hierarchy will remain working even when faced with using a new derivation.  The principle has structural requirements and behavioral requirements that must be followed to apply the principle correctly.  As with most things, the easiest way for me to wrap my head around a concept is via examples.  Let’s tackle the structural requirements first and look at example violations.

Consider the following base class:

using System.Collections.Generic;

namespace LSP
{
    public abstract class WidgetBase
    {
        public virtual IList<int> SomeList(string title)
        {
            //Some useful boilerplate implementation
            return new List<int>();
        }
    }
}

It is a simple abstract class with one method (for now) named SomeList that accepts a string argument and returns a generic IList of type int.  Messing with the signature would certainly be a violation of the principle but the good news is the compiler saves us from that, changing the parameter type or return type in our derivation will not compile.

using System.Collections.Generic;

namespace LSP
{
    public class BrokenWidget:WidgetBase
    {
        public override IList<int> SomeList(object title)
        {
            return new List<int>();
        }

        public override IList<object> SomeList(string title)
        {
            return new List<object>();
        }
    }
}

If we are only considering code reuse we might be tempted to say something like “My derivation is almost like a widget but I don’t have the notion of ‘SomeList’ so I just won’t implement it”.  This will compile:

using System;
using System.Collections.Generic;

namespace LSP
{
    public class AlmostAWidget:WidgetBase
    {
        public override IList<int> SomeList(string title)
        {
            throw new NotImplementedException(); 
        }
    }
}

Consider a consumer using an Inversion of Control container to resolve WidgetBase, it will be perfectly happy getting any concrete widget that returns an IList<int> when it calls SomeList but if it gets a hold of the AlmostAWidget class above, it will get an unexpected runtime error when SomeList is called and probably appear to be broken.  Throwing this unexpected exception could be considered structural or behavioral, it doesn’t really matter as the result is the same.  A poor derivation broke working code.

Here is a subtle problem with C# array covariance.  in C#, covariance for arrays enables implicit conversion of an array of a more derived type to an array of a less derived type. But this operation is not type safe meaning it will compile but can cause problems that are only discovered at run time.  As an example, we will add the following method to WidgetBase:

        public virtual WidgetBase[] Relatives()
        {
            return new WidgetBase[0];
        }

Now let’s add two more derivations to our Model, NotLspWidget which makes use of array covariance:

namespace LSP
{
    public class NotLspWidget:WidgetBase
    {
        public override WidgetBase[] Relatives()
        {
            return new NotLspWidget[1] { this };
        }
    }
}

And LspWidget which does not:

namespace LSP
{
    public class LspWidget : WidgetBase
    {
        public override WidgetBase[] Relatives()
        {
            return new WidgetBase[1] { this };
        }
    }
}

Notice that with NotLspWidget the backing store for the Relatives array is an array of type NotLspWidget (the derived type) but with LspWidget the store is an Array of type WidgetBase,

Now consider the following unit tests which will act as our consumer code (the code we, as good designers, are trying not to break by applying the Liskov Substitution Principle rules correctly to our object hierarchy).

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using LSP;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestLspCovariance()
        {
            WidgetBase w = new NotLspWidget();
            WidgetBase[] relatives = w.Relatives();

            //try to put some other WidgetBase to the array
            relatives[0] = new LspWidget();
            Assert.IsTrue(true);
        }
        [TestMethod]
        public void TestLspCovariance2()
        {
            WidgetBase w = new LspWidget();
            WidgetBase[] relatives = w.Relatives();

            //try to put some other WidgetBase to the array
            relatives[0] = new NotLspWidget();
            Assert.IsTrue(true);
        }
    }
}

The tests are really simple and similar.  In test one I get the relatives from the NotLspWidget, then try to add and LspWdiget to the array but we get a runtime exception:

TypeMismatch

Test two does the exact reverse and works perfectly well because the designer of LspWidget did not rely on covariance to “convert” the returned array.  This sounds like a contrived example but if our consumers are using abstractions, as they should be, they should be able to add abstract types to arrays of what appear to be abstract types without worrying about this bit of .Net trivia.

Now let’s handle the more difficult application of the behavior portion of the principle.  For this we must consider the reasonable assumptions consumers of our object hierarchy will make.

Method Hiding is a pretty nasty violation of the Liskov Substitution Principle that can be particularly difficult to chase down.  For this example we will add The following method to WidgetBase:

        public virtual long DoMath(int number)
        {
            return number * number;
        }

Now we hide this method in NotLspWidget by using the new key word and changing the algorithm.  Note, there is nothing wrong with changing the algorithm!

        public new long DoMath(int number)
        {
            return number + number;
        }

Now execute this simple test:

        [TestMethod]
        public void TestHiddenMethod()
        {
            NotLspWidget w = new NotLspWidget();
            Assert.AreEqual(w.DoMath(3), ((WidgetBase)w).DoMath(3));
        }

The reasonable assumption is that if I call DoMath on NotLspWidget and DoMath on NotLspWidget upcasted to WidgetBase the answer is the same.  Execute the unit test and you will find that a different DoMethod is called depending if you called it via the concretion or a reference upcasted to the base type.

The final example I want to discuss is purely one of convention.  Let’s say our object hierarchy contains a lot of generic lists and these lists are frequently null or empty.  As the designer of the hierarchy we may choose to make it a policy that all methods that return Lists<T> shall return an empty list rather than a null.  This behavior would be documented for our consumers.  In fact I do prefer this approach to lists as now it removes the burden of checking for null on all methods that return a list from my consumers and makes for a much cleaner code base.  What happens if a derivation comes along and violates this policy by implementing the following (or any other code that can result in a null being returned)?

        public override System.Collections.Generic.IList<int> SomeList(string title)
        {
            return null;
        }

All consumers of this code will likely break.

In summary what constitutes a Liskov Substitution Principle violation?

  • Throwing Not Implemented Exceptions.
  • Making use of Method Hiding (the New keyword on virtual methods)
  • Making use of array covariance on public return types
  • Violating the implied or documented behavior of a base class (returning null instead of empty lists for example)

For more information on LSP and other design principles, contact Intertech’s team.