Liskov Substitution Principle in C# – SOLID Design Principles – Part 3

Posted on Updated on


Overview

In our introduction to the SOLID Design Principles, we mentioned the Liskov Substitution Principle as one of the five principles specified. In this post we are going to dive into this design principle and work with a very simple example in C#.

As you study the SOLID Design Principles, you will notice there is a great deal of overlap among the individual principles. While discussing the Liskov Substitution Principle, we are going to take a quick dive into the Interface Segregation Principle as well, but not overly so as the next post will discuss that more deeply. We will however take a look at how to segregate interfaces in pursuit of not violating the Liskov Substitution Principle.

Formal Definition

The Liskov Substitution Principle states that if for each object m1 of type S there is an object m2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when m1 is substituted for m2 then S is a subtype of T.

Useful Definition

Let’s reword the definition above to actually be understandable. In a nutshell, the Liskov Substitution Principle  includes two main points:

  1. Derived classes should be substitutable for their base classes (or interfaces).
  2. Methods that use references to base classes (or interfaces) have to be able to use methods of the derived classes without knowing about it or knowing the details.

So what in the world does all that mean? It means that our derived classes (or implementing classes) cannot modify or break the functionality dictated by their base classes or implemented interfaces.

Let’s just dive in and take a look at the Engine/Starter class design that we have been using for the first two posts on the SOLID Design Principles.

As of the completion of our post on the Open-Closed Principle, here is our design:

Open Closed Principle

 

We created separate concrete classes for the different types of Starters used to start our Engine and we made each class implement the IStarter interface. This allowed us to migrate our code to be open to extending but closed to modification.

So far so good, right? Well, maybe not!

Let’s look at the code from the last post that matches the class diagram above:

public class Engine
{
    public IgnitionResult Start(IStarter starter)
    {
        return starter.Start();
    }
}

public interface IStarter
{
    string Brand { get; set; }
    string Model { get; set; }
    IgnitionResult Start();
}

public class ElectricStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the electric starter
        return IgnitionResult.Success;
    }
}

public class PneumaticStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the pneumatic starter
        return IgnitionResult.Success;
    }
}

public class HydraulicStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the hydraulic starter
        return IgnitionResult.Success;
    }
}

public enum IgnitionResult
{
    Success,
    Failure
}

Design #1 – Not Good At All!

This all works fine in the simple example because we are assuming that all types of Starters are the same. But what if they aren’t? What if, for example an ElectricStarter makes use of a Battery, a PneumaticStarter makes use of an AirCompressor, and a HydraulicStarter requires a HydraulicPump object to function? Well, if we were hell-bent on keeping our IStarter interface we could do something like this:

First we would have to create classes for Battery, AirCompressor, and HydraulicPump. Then we would add properties for each to the IStarter interface. Let’s just do it and see how it looks 😦

public interface IStarter
{
    string Brand { get; set; }
    string Model { get; set; }
    Battery Battery { get; set; }
    AirCompressor Compressor { get; set; }
    HydraulicPump Pump { get; set; }
    IgnitionResult Start();
}

public class ElectricStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public Battery Battery
    {
        get;
        set;
    }

    public AirCompressor Compressor
    {
        get
        {
            throw new NotImplementedException("An Electric Starter does not use an AirCompressor.");
        }
        set
        {
            throw new NotImplementedException("An Electric Starter does not use an AirCompressor.");
        }
    }

    public HydraulicPump Pump
    {
        get
        {
            throw new NotImplementedException("An Electric Starter does not use a HydraulicPump.");
        }
        set
        {
            throw new NotImplementedException("An Electric Starter does not use an HydraulicPump.");
        }
    }

    public IgnitionResult Start()
    {
        //code here to initiate the electric starter
        return IgnitionResult.Success;
    }
}

public class PneumaticStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public Battery Battery
    {
        get
        {
            throw new NotImplementedException("An PneumaticStarter does not use a Battery.");
        }
        set
        {
            throw new NotImplementedException("An PneumaticStarter does not use an Battery.");
        }
    }

    public AirCompressor Compressor
    {
        get;
        set;
    }

    public HydraulicPump Pump
    {
        get
        {
            throw new NotImplementedException("An PneumaticStarter does not use a HydraulicPump.");
        }
        set
        {
            throw new NotImplementedException("An PneumaticStarter does not use an HydraulicPump.");
        }
    }

    public IgnitionResult Start()
    {
        //code here to initiate the pneumatic starter
        return IgnitionResult.Success;
    }
}

public class HydraulicStarter : IStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public Battery Battery
    {
        get
        {
            throw new NotImplementedException("A HydraulicStarter does not use a Battery.");
        }
        set
        {
            throw new NotImplementedException("A HydraulicStarter does not use an Battery.");
        }
    }

    public AirCompressor Compressor
    {
        get
        {
            throw new NotImplementedException("A HydraulicStarter does not use an AirCompressor.");
        }
        set
        {
            throw new NotImplementedException("A HydraulicStarter does not use an AirCompressor.");
        }
    }

    public HydraulicPump Pump
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the hydraulic starter
        return IgnitionResult.Success;
    }
}

public class Battery
{
}

public class AirCompressor
{
}

public class HydraulicPump
{
}

public enum IgnitionResult
{
    Success,
    Failure
}

Alright! So we added the properties to our IStarter interface and implemented them in each concrete class, so all is well, right? Not exactly.

Although we aren’t necessarily violating the Open-Closed Principle, we are in violation of the Liskov Substitution Principle and honestly this is just really not a good design! Although we implemented all of the possible properties of our classes in our IStarter interface, the classes that implement our interface are NOT substitutable for it. Why not? Because their behavior is not consistent across the different types of starters. Although the interface is in place, the implementing classes don’t truly implement it when in play.

Consider this: suppose we create an instance of an ElectricStarter and use an instantiated Battery object to set its Battery property? That works pretty well. But now let’s imagine another developer consuming our code and seeing the Compressor property as being a usable public property for the ElectricStarter and trying to either get it or set it. What happens? A NotImplementedException is thrown 🙂 This is not consistent behavior! The interface contains a property for an AirCompressor, a developer consuming our code can reasonably expect that to work, despite the fact that any of us “reasonable” people might look at that and ask “why would an ElectricStarter need an AirCompressor?” 🙂

So you may ask “why not just remove the NotImplementedExceptions in the properties that don’t matter for each class and just go with it?” The answer to that is easy – any time you see properties or methods within derived (or implementing) classes that do not do what their name suggests (or what they should be doing) that is a big red flag or a not-so-good design! As responsible designers/developers, we must handle invalid conditions and operations correctly and deliberately and not place properties or methods in our interfaces or classes that surprise their consumers by what they do or how they behave – make sense? Thus instead of having a property or method that actually does nothing is not a good idea and the presence of such items signals trouble!

The Liskov Substitution Principle is very focused on extrinsic public behavior and the fact that consumers expect predictable and constant behavior for published/implemented interfaces. Our design does NOT provide that!

Design #2 – Much Better

To rectify this flawed design, we need to think about another SOLID design principle – the Interface Segregation Principle. If we think about our IStarter interface as the starting point, we can consider the following things:

  1. The properties/method that are common to ALL starter types are: Brand, Model, and Start().
  2. The properties that are specific to each starter type are: Battery, Compressor, and Pump.

With these things in mind, the first thing that we need to do is to make use of not one interface but four! Take a look at our revised interfaces below:

public interface IStarter
{
    string Brand { get; set; }
    string Model { get; set; }
    IgnitionResult Start();
}

public interface IElectricStarter : IStarter
{
    Battery Battery { get; set; }
}

public interface IPneumaticStarter : IStarter
{
    AirCompressor Compressor { get; set; }
}

public interface IHydraulicStarter : IStarter
{
    HydraulicPump Pump { get; set; }
}

I picked the interface segregation as our starting point because it sets the stage for the changes that need to be made to the concrete classes. This design is far superior from the standpoint of the Liskov Substitution Principle!

So now let’s rewrite our Starter classes to implement their respective interfaces:

public class ElectricStarter : IElectricStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public Battery Battery
    {
        get;
        set;
    }       

    public IgnitionResult Start()
    {
        //code here to initiate the electric starter
        return IgnitionResult.Success;
    }
}

public class PneumaticStarter : IPneumaticStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public AirCompressor Compressor
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the pneumatic starter
        return IgnitionResult.Success;
    }
}

public class HydraulicStarter : IHydraulicStarter
{
    public string Brand
    {
        get;
        set;
    }

    public string Model
    {
        get;
        set;
    }

    public HydraulicPump Pump
    {
        get;
        set;
    }

    public IgnitionResult Start()
    {
        //code here to initiate the hydraulic starter
        return IgnitionResult.Success;
    }
}

We now have a design that makes far more sense. Yes, we went from one interface to four, but this is not a bad thing when we consider the SOLID Design Principles collectively, which is a very good thing to do.

In the next post, we are going to dive more deeply into the Interface Segregation Principle. See the links below for all posts related to the SOLID Design Principles.

The SOLID Design Principles
The Single Responsibility Principle (SRP)
The Open-Closed Principle (OCP)
The Liskov Substitution Principle (LSP)
The Interface Segregation Principle (ISP)
The Dependency Inversion Principle (DIP)

 

3 thoughts on “Liskov Substitution Principle in C# – SOLID Design Principles – Part 3

    Kobie D Williams said:
    June 28, 2018 at 6:37 pm

    Very Very Clear

    Like

    kabirw said:
    June 28, 2018 at 6:39 pm

    Very Very Clear Explanation

    Like

      johnnels responded:
      June 30, 2018 at 1:16 am

      Thank you for your kind words!

      Like

Leave a reply to Kobie D Williams Cancel reply