SOLID In C# – Liskov Principle

This article is part of a series on the SOLID design principles. You can start here or jump around using the links below!

S – Single Responsibility
O – Open/Closed Principle
L – Liskov Substitution Principle
I – Interface Segregation Principle
D – Dependency Inversion


What Is The Liskov Principle?

The Liskov Principle has a simple definition, but a hard explanation. First, the definition :

Types can be replaced by their subtypes without altering the desirable properties of the program.

So basically if I have something like this :

class MyType
{
    //Some code here
}

class MySubType : MyType
{
    //Some code here
}

class MyService
{
    private readonly MyType _myType;

    public MyService(MyType myType)
    {
        _myType = myType;
    }
}

If in the future I decide that MyService should depend on MySubType instead of MyType, theoretically I shouldn’t alter “the desirable properties of the program”. I put that in quotes because what does that actually mean? A large part of inheritance is extending functionality and therefore by definition it will alter the behaviour of the program in some way.

That last part might be controversial because I’ve seen plenty of examples of Liskov that state that overriding a method in a sub class (which is pretty common) to be a violation of the principle but I feel like in reality, that’s just going to happen and isn’t a design flaw. For example :

class MyType
{
    public virtual void DoSomething()
    {

    }
}

class MySubType : MyType
{
    public override void DoSomething()
    {
        //Do some extended functionality. 

        //Maybe I leave this here. But should it go at the start or at the end?
        base.DoSomething();
    }
}

Is this a violation? It could be because substituting the subtype may not “break” functionality, but it certainly changes how the program functions and could lead to unintended consequences. Some would argue that Liskov is all about behavioural traits, which we will look at later, but for now let’s look at some “hard” breaks. e.g. Things that are black and white wrong.

Liskov On Method Signatures

The thing is, C# is actually very hard to violate Liskov and that’s why you might find that examples out there are very contrived. Indeed, for the black and white rules that we will talk about below, you’ll notice that they are things that you really need to go out of your way (e.g. ignore compiler warnings) to break. (And don’t worry, I’ll show you how!).

Our black and white rules that no one will argue with you on Stackoverflow about are….

A method of a subclass can accept a parent type as a parameter (Contravariance)

The “idea” of this is that a subclass can override a method and accept a more general parameter, but not the opposite. So for example in theory Liskov allows for something like this :

class MyParamType
{

}
class MyType
{
    public virtual void DoSomething(MyParamType something)
    {

    }
}

class MySubType : MyType
{
    public override void DoSomething(object something)
    {
    }
}

But actually if you try this, it will blow up because in C# when overriding a method the method signature must be exactly the same. Infact if we remove the override keyword it just acts as a method overload and both would be available to someone calling the MySubType class.

A method of a subclass can return a sub type as a parameter (Covariance)

So almost the opposite of the above, when we return a type from a method in a subclass, Liskov allows it to be “more” specific than the original return type. So again, theoretically this should be OK :

class MyReturnType
{

}
class MyType
{
    public object DoSomething()
    {
        return new object();
    }
}

class MySubType : MyType
{
    public MyReturnType DoSomething()
    {
        return new MyReturnType();
    }
}

This actually compiles but we do get a warning :

'MySubType.DoSomething()' hides inherited member 'MyType.DoSomething()'. Use the new keyword if hiding was intended.

Yikes. We can get rid of this error message by following the instruction and changing our SubType to the following :

class MySubType : MyType
{
    public new MyReturnType DoSomething()
    {
        return new MyReturnType();
    }
}

Pretty nasty. I can honestly say in over a decade of using C#, I have never used the new  keyword to override a method like this. Never. And I feel like if you are having to do this, you should stop and think if there is another way to do it.

But the obvious issue now is that anything that was expecting an object back is now getting this weird “MyReturnType” back. Exceptions galore will soon follow. This is why it makes it to a hard rule because it’s highly likely your code will not compile doing this.

Liskov On Method Behaviours

Let’s jump into the murky waters on how changing the behaviour of a method might violate Liskov. As mentioned earlier, I find this a hard sell because the intention of overriding a method in C#, when the original is marked as virtual, is that you want to change it’s behaviour. It’s not that C# has been designed as an infallible language, but there are pros to inheritance.

Our behavioural rules are :

Eceptions that would not be thrown normally in the parent type can’t then be thrown in the sub type

To me this depends on your coding style but does make sense in some ways. Consider the following code :

class MyType
{
    public virtual void DoSomething()
    {
        throw new ArgumentException();
    }
}

class MySubType : MyType
{
    public override void DoSomething()
    {
        throw new NullReferenceException();
    }
}

If you have been using the original type for some time, and you are following the “Gotta catch ’em all” strategy of exceptions and trying to catch each and every exception, then you may be intending to catch the ArgumentException. But now when you switch to the subtype, it’s throwing a NullReferenceException which you weren’t expecting.

Makes sense, but the reality is that if you are overriding a method to change the behaviour in some way it’s an almost certainty that new exceptions will occur. I’m not particularly big on this rule, but I can see why it exists.

Pre-Conditions cannot be strengthened and Post-Conditions cannot be weakened in the sub type

Let’s look at pre-conditions first. The idea behind this is that if you were previously able to call a method with a particular input parameter, new “rules” should not be in place that now rejects those parameters. A trivial example might be :

class MyType
{
    public virtual void DoSomething(object input)
    {
    }
}

class MySubType : MyType
{
    public override void DoSomething(object input)
    {
        if (input == null)
            throw new NullReferenceException();
    }
}

Where previously there was no restriction on null values, there now is.

Conversely, we shouldn’t weaken the ruleset for returning objects. For example :

class MyType
{
    public virtual object DoSomething()
    {
        object output = null;
        //Do something

        //If our ouput it still null, return a new object. 
        return output ?? new object();
    }
}

class MySubType : MyType
{
    public override object DoSomething()
    {
        object output = null;
        //Do something
        return output;

    }
}

We could previously rely on the return object never being null, but now we may be returned a null object.

Similar to the exceptions rule, it makes sense that when extending behaviour we might have no choice but to change how we handle inputs and outputs. But unlike the exceptions rule, I really like this rule and try and abide by it.

Limiting General Behaviour Changes

I just want to quickly show another example that possibly violates the Liskov principle, but is up to interpretation. Take a look at the following code :

class Jar
{
    public virtual void Pour()
    {
        //Pour out the jar. 
    }
}

class JarWithLid : Jar
{
    public bool IsOpen { get; set; }

    public override void Pour()
    {
       if(IsOpen)
       {
            //Only pour if the jar is open. 
       }
    }
}

A somewhat trivial example but if someone is depending on the class of “Jar” and calling Pour, everything is going fine. But if they then switch to using JarWithLid, their code will no longer function as intended because they are required to open the Jar first.

In some ways, this is covered by the “pre-Conditions cannot be strengthened” rule. It’s clearly adding a pre-existing condition that the jar must be opened before it can be poured. But on the other hand, in a more complex example a method may be hundreds of lines of code and have various new behaviours that affect how the caller might interact with the object. We might classify some of them as “pre-conditions” but we may also just classify them as behavioural changes that are inline with typical inheritance scenarios.

Overall, I feel like Liskov principle should be about limiting “breaking” changes when swapping types for subtypes, whether that “breaking” change is an outright compiler error, logic issue, or unexpected behavioural change.

What’s Next?

Up next is the I in SOLID. That is, The Interface Segregation Principle.

Leave a Comment