Liskov Substitution Principle

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)

 

SOLID Design Principles

Posted on Updated on

In software development, principles differ from patterns in the sense that where patterns represent complete, identifiable, repeatable solutions to common problems, principles are objective, factual statements that can be made about code and the manner in which it is constructed and the overall design of an implementation. In other words, patterns refer to code scenarios while principles refer to qualities of code and these qualities are useful in identifying the value of the code.

In this post, we are going to be introduced to Bob Martin’s SOLID design principles. These principles have been around for a long time, and it is immeasurably important for every object-oriented developer to understand them and use them in making day-to-day design decisions. It is not that uncommon to see developers dive into development tasks by writing code first and considering architecture and design second. A good developer though inverts this scenario and considers a sound design before writing the first line of code! This prevents undue code maintenance pain later and allows for the development of better applications through and through.

Consideration of design principles is extremely important throughout a development effort, and failure to make the proper considerations can have a devastating effect on the development of the application and the application’s usefulness, performance, and maintainability.

What are the SOLID Design Principles?

In this section, we are going to outline and briefly discuss each of the five design principles. In subsequent posts we will dive into each principle in more detail. The five SOLID design principles are listed below:

  1. The Single Responsibility Principle (SRP) – this principle states that there should never be more than one reason to change a class. This means also that a given class should exist for one and only one purpose.
  2. The Open-Closed Principle – this principle states that modules should be open for extension but closed for modification. This seems a bit unclear at first until you realize that you can change an object’s behavior by either using abstractions, implementing common interfaces, and from inheritance from common base classes or extending abstract base classes.
  3. The Liskov Substitution Principle – this principle, introduced by Barbara Liskov, simply-stated means that derivative classes have to be substitutable for their base classes. If we think about this for a minute, we can see that this also means that any class that implements a specific interface can be replaced by any other class that implements that interface. In other words, it can be substituted for the original class. Furthermore, a derived class must honor the ‘contract’ set by its super class.
  4. The Interface Segregation Principle (ISP) – this principle states that objects should not be forced to implement interfaces that they do not use. Though this may sound obvious based on the wording, in practice it really means that interfaces should be finely-grained and not specific to the classes for which their implementation is intended.
  5. The Dependency Inversion Principle (DIP) – this principle states that higher-level classes should not depend upon lower-level classes but that both should depend on abstractions. Furthermore, these abstractions should not depend on concrete details but rather on abstractions as well.

Though each principle can be discussed individually, we should understand that no single principle should exist or be applied by itself. They should all be considered as part of the design process. In the following posts, we will dive into each principle in detail and take a look at some simplified code examples that illustrate each one.

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)