SOLID Principles: Liskov Substitution Principle (LSP) In Practice

/, Best Practices, C#, Design/SOLID Principles: Liskov Substitution Principle (LSP) In Practice
This entry is part 5 of 6 in the series SOLID

LSP Definition

The official definition from Barbara Liskov sounds like this:

“If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program” – B. Liskov.

It’s not very hard to grasp what this definition means but I’ll quote the definition of LSP from the book of Uncle Bob “Agile Principles, Patterns, and Practices in C#” just in case. We find there the following definition of the Liskov Substitution Principle:

“The Liskov Substitution Principle states that Subtypes must be substitutable for their base types.”

By the way, Barbara Liskov is a scientist who described this principle in 1988. Let’s try to clarify what LSP means for developers in practice. What does it mean to be substitutable for a class? Let’s pretend that we have a client which uses the API of interface B.

Inheritance

There are two implementors of that interface B. It is implemented by classes A and C. Class C can be considered substitutable for class A if the client doesn’t observe any difference using the class C instead of A. From the client’s perspective, a client shouldn’t experience different behavior using different inheritors of the same base class or an interface.

Actually, the LSP is strongly related to how object-oriented languages allow us to use inheritance.

A derives from B

If class B inherits from A, then we can use B as A but we can’t use A as B. Thus, clients of A can use both A and B even if they know nothing about B. This is basics of polymorphism. Despite that this is basics, developers all the time violate the logic of such a relationship. By the way, as a remark, I want to remind you that it’s not necessary to have such a mechanism as the inheritance to model such relationships between objects. In dynamically typed languages, we use the so-called Duck Typing. If a bird swims like a duck, it quacks like a duck then I call that bird a duck. That’s why the term “Duck Typing” appeared. Dynamically typed languages don’t know if we’re allowed to call a certain method or not. The runtime checks if an object can respond to a message. By saying “message”, I mean a call to a method since in Dynamically Typed Languages it’s better to say “send a message” rather than “call a method.” Ok, let’s get back to the main topic.

LSP Violation

There are two ways of breaking the substitutability:

  • Violating a Contract
  • Violating Covariance\Contravariance

For a significant number of developers, it’s unclear what does it mean, so it requires further clarification. Without understanding these fundamental concepts, you’ll not be able to design types which don’t violate LSP, sooner or later you’ll bump into the horrible problems caused by an LSP violation. So, keep patience and let’s discuss a little bit the notion of contracts.

Contracts

Programming to Contracts was elaborated by Bertrand Meyer. In his classic book “Object-Oriented Software Construction”, he described the advantages of programming to contracts. He even came out with a whole new language called “Eiffel” which was built with programming to contracts in mind.

So, what is a contract? Many developers mix the notions of an interface and a contract. This statement becomes more convincing with the fact that in WCF we treat interfaces and contracts equally. In WCF, a service contract can only be represented by an interface. In the real world, including real world outside of programming, contracts have some semantic payload. Usually, they determine some kind of relationships between people, rights, objects and so on. Interfaces have no semantic payload. They determine nothing except signatures. But signatures don’t bear any significant semantic payload. An interface represents just a shape. Thus, interfaces are not contracts.

Here is an example of a contract provided by Krzysztof Cwalina:

public abstract class CollectionContract<T> : IList<T> {
    public void Add(T item) {
        AddCore(item);
        count++;
    }
 
    public int Count {
        get { return count; }
    }
 
    protected abstract void AddCore(T item);
 
    private int count;  
    ...
}

This contract says that when an item is added to the collection, the Count property is incremented by one. In addition, this contract is locked for all subtypes.

A contract of a method constitutes of the following parts:

  • Acceptable and unacceptable input values or types, and their meanings
  • Return values or types, and their meanings
  • Error and exception condition values or types that can occur, and their meanings
  • Side effects
  • Preconditions
  • Postconditions
  • Invariants

From the learning perspective, we are mostly interested in Preconditions, Postconditions, and Invariants. Preconditions of a function are the set of requirements a function applies to the input parameters and sometimes the state of the object to which that function belongs.

I have a simple example from my own practice. It is, of course, simplified and presented in a form of a skeleton for the sake of simplicity.

public interface IBankTerminal1PaymentGateway
{
    uint ProcessPayment(decimal amount);
}
public interface IBankTerminal2PaymentGateway 
{ 
    uint ProcessPayment(decimal amount, string uniqueId); 
}
public interface IBankTerminal
{
    int ProcessPayment(decimal amount, string uniqueId);
}   
public class BankTerminal1 : IBankTerminal
{
    private IBankTerminal1PaymentGateway _gateway;
                
    ///<returns>Response Code. Always >= 0</returns>
    public int ProcessPayment(decimal amount, string uniqueId)
    {
        //doesn't require uniqueId at all
        return (int)_gateway.ProcessPayment(amount);
    }
}

public class BankTerminal2 : IBankTerminal
{
    private IBankTerminal2PaymentGateway _gateway;
    public int ProcessPayment(decimal amount, string uniqueId)
    {
        if (string.IsNullOrWhiteSpace(uniqueId))
        {
            throw new ArgumentException("A client must provide a unique ID for BankTerminal2");
        }       
        return _gateway.ProcessPayment(amount, uniqueId);
    }
}

Here we have two different bank terminal models. Both models derive from the IBankTerminal interface which defines the ProcessPayment operation.

These terminals share a significant amount of business logic and they have very similar interfaces, so developers decided to join them by introducing that IBankTerminal interface. Unfortunately, both terminals have their own payment gateways with different interfaces. One requires a uniqueId to be generated by a client, while another doesn’t. To make the API universal, developers just rolled out a single method with two parameters.
The implementation of the first terminal just ignores the second parameter, while the second checks the uniqueId and if it’s null or white space, it throws the ArgumentException.
A client doesn’t expect such a behavior working with the base class. This happens because the second inheritor strengthens the preconditions. The first terminal accepts the uniqueId which can be equal to any value, no restrictions were applied.
The corollary is that a client has to be aware of different behavior of these bank terminals. Those clients who worked with the first terminal would not expect any exceptions working with the second one passing null as a unique id.

The overall idea is that by weakening postconditions, inheritors extend the possible outcome from a function. As a result, clients face unexpected situations or they, in the end, have to understand different cases and treat them in a specific way.

You can write contracts on the .NET platform in C# with a special library called “Code Contracts”, not surprisingly. Using code contracts, you can harness the power of static code verification on correctness. Due to some practical problems and problems related to the poor implementation of the “Code Contracts” library, it’s not a very popular approach nowadays. For example, Code Contracts work very slow on big solutions. And this is only one of the reasons. This library is out of the scope of the course, so if you’re interested in “Code Contracts” you can just google for it.

Another problem I want to demonstrate is related to the violation of invariants on a classic example in the next lecture.

Violation of Invariants

Let’s consider a classic example of LSP violation. I love this example because of two reasons. The first one is that it actually demonstrates the LSP violation and this reason is obvious and the second reason is that it shows that Object-Oriented Programming often can’t directly map the relationships between objects in the real world into the same model of relationships between them in code.

A great number of developers think that writing code in OOP language, they’re modeling real-world domain problems. And partly this is true. But the real-world relationships between objects sometimes can’t be modeled directly in OOP language. Here is one of the naïve OOP statements: “child classes implement IS-A relationship with base classes”. For example, Dog is an Animal. Cat is an Animal. So, we can create three classes. Animal is the base class and the other two (Cat and Dog) are inheritors. Unfortunately, sometimes this kind of design doesn’t work as expected and you’ll see this in a minute.

So, let’s reflect the relationships between two real-world notions in code. We want to implement a Rectangle and Square, being able to calculate their areas. I’m sure you’ll agree with me that in the real world, Square is a special case of Rectangle. It’s obvious because Square is Rectangle with equal sides.

So, we can write the following:

public class Rectangle
{
    public int Width { get; set; }
    public int Height { get; set; }
}

public class Square : Rectangle
{
}

I created the Rectangle class which has two properties: Width and Height and the Square which inherits from the Rectangle class. Separately I will create the class which is responsible for calculating areas of that shapes.

public class AreaCalculator
{
    // or maybe it'd be better to multiply Widths?       
    public static int CalcSquare(Square square) => square.Height * square.Height;
    public static int CalcRectangle(Rectangle square) => square.Height * square.Width;
}

Looks fine from the first sight. I’ll pretend that I’m a client and I want to create two shapes and calculate their areas. For that, I’ll implement the Main method where all the things will happen.

class Program
{
    static void Main()
    {
        Rectangle rect = new Rectangle() { Width = 2, Height = 5 };
        int rectArea = AreaCalculator.CalcRectangle(rect);
        Console.WriteLine($"Rectangle Area = {rectArea}");

        //

        Rectangle square = new Square() { Height = 2, Width = 10 };
        int squareArea = AreaCalculator.CalcRectangle(square);
        Console.WriteLine($"Square Area = {squareArea}");
    }
}

Pay attention to this:

Rectangle square = new Square() {Height = 2, Width = 10 };

Wait a minute. What the hell is this? How a Square can contain sides of different length?
I hope that it’s obvious that something is wrong here with the API of Square. Square allows to set different length to Width and Height.

This design clearly violates the Liskov Substitution Principle. Square is not substitutable for Rectangle. Rectangle implements the invariant which states that width and height could be of different length. Square has to implement another invariant which states that Width and Height have to be equal. I intentionally omit that actually they also have to be greater or equal to zero.
There is another problem is hiding here. Most likely you would need to create methods which take rectangle as a parameter and implement some business logic. In case of calculating the area, such a method could look like this.

public static int CalcArea(Rectangle rect)
{
    if (rect is Square)
    {
        return rect.Height * rect.Height;
    }
    else
    {
        return rect.Height * rect.Width;
    }
}

This method checks the type inside the method and run the appropriate calculation algorithm.
What do you think? Is it a good way to solve the problem of LSP violation? Doesn’t this fix resemble you something we’ve seen previously? Yes, indeed, this is a violation of the Open/Closed principle. This method is open for modification. What will happen if we introduce a new shape? Correct, we will have to modify that switch-statement. This example of LSP violation is a hidden violation of OCP.

Remember that inheritors can require less and guarantee more but not vice-versa. The smell which we observe here is also called “Refused Bequest”. Why at all the problem arise in the way it actually arises here? Were we wrong saying that a square is a special case of a rectangle? No, we were right, this is ridiculous to think what we were wrong. Any child knows that a Square is a subtype of a Rectangle. So why we face this problem directly modeling such a relationship between square and rectangle abstractions?

The answer is quite irritating; the thing is that the programming code just represents the concepts of square and rectangle. The class Rectangle is not a rectangle, and the class Square is not a Square, they are just representatives. Everything is fine actually, the thing is that representatives in the real world also don’t share the relationships between things they represent. So, how can we fix such a poor design?

Refactoring to Remove LSP Violation

Obviously, we need to make impossible to set different length of sides for a square to fix the problem. For now, the expectations of the square’s clients are broken. The AreaCalculator class now demonstrates that there is a problem of separated data and behavior. The calculation algorithms are separated from the Rectangle and Square classes which own the data required for calculations. The AreaCalculator can’t exist without Rectangle and Square, so why the behavior was moved away from Rectangle and Square? Rectangle and Square for now obviously lack cohesion. To cure the decease, we as always should construct proper abstractions. What we want to abstract away is the business logic of calculating the area. Each shape has its own algorithm of calculation the area. It is much better to make behavior shared rather than data.

Let’s abstract away the CalculateArea method by introducing the IShape interface.

public interface IShape
{
    int CalculateArea();
}

I want the Rectangle and Square to inherit from the IShape implementing their own algorithms.

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }

    public int CalculateArea() => Width * Height;
}

public class Square : IShape
{
    public int SideLength { get; set; }

    public int CalculateArea() => SideLength * SideLength;
}

Square now defines its own property named SideLength. Now a client can’t misuse their API. In case of Rectangle, calling to CalculateArea, a client will get the area of a Rectangle, while in case of Square, a client will get the area of a Square. The client can’t set different length of width and height to a square.

More Examples of LSP Violation

Variance

We omitted the discussion of a very important topic – variance. Variance is a very complicated concept, so we’re going to only touch this topic despite that it is related to the LSP.

  • Variance is the notion that describes the types compliance. It branches out to two notions: covariance and contravariance.
  • Assuming the type A can be cast to type B, type X is covariant in case X<A> can be cast to X<B>.

For example, IBar is covariant to T, if the following is true:

IBar s = …;
IBar<object> o = s;

This code snippet shows that “s” is covariant to “o”. Here a simple example which demonstrates the problem of casting: assuming we have the following class hierarchy:

class Animal { }
class Dog : Animal { }
class Cat : Animal { }

and a naïve generic implementation of Stack:

public class Stack<T>
{
    private int position;
    readonly T[] data = new T[100];
    public void Push(T obj)
    {
        data[position++] = obj;
    }

    public T Pop()
    {
        return data[--position];
    }
}

The following code will not compile in C#:

Stack<dog> dogs = new Stack<dog>();
Stack<animal> animals = dogs; // compilation error

C# deprecates this to prevent the possibility of writing the following code:

animals.Push(new Cat()); //adding a cat to dogs

We’re trying here to add cats to dogs. Arrays in C# are covariant because of historical reasons.
So, the following code will compile:

Dog[] dogs = new Dog[10];
Animal[] animals = dogs;

And the violation of the LSP will be detected only in the runtime:

animals[0] = new Cat(); //runtime exception will occur

Starting from C# 4, generic interfaces allow variance though special keywords “in” and “out”. Generic classes at the same time don’t allow variance. The “out” keyword guarantees that inside the implementation of that interface, a generic parameter can only be used in the return statements. It solves the problem we’ve seen with adding a cat to a stack of dogs. This can be expressed as in the following snippet:

public interface IPoppable<out T> 
{ 
    T Pop(); 
}

If the Stack class from the previous example implements this interface, then the following code will compile and be absolutely correct:

var dogs = new Stack<dog>();
dogs.Push(new Dog());
IPoppable<animal> animals = dogs; //allowed

On the contrary, we can use the “in” keyword to ensure that the generic parameter is only used as the input. Here is the code snippet, which demonstrates the idea:

public interface IPushable<in T> 
{ 
    void Push(T val); 
}

If the Stack class implements this interface, then the following code will compile and be absolutely correct:

IPushable<animal> animals = new Stack<animal>();
IPushable<cat> cats = animals; //allowed
cats.Push(new Cat());

NotSupportedException Smell

Another example I want to show you comes from the BCL of the .NET framework. It is also a well-known example of the LSP violation. There is a generic interface named ICollection<T> defined in the BCL. This type derives from the IEnumerable<T> and it defines the following methods:

  • Add
  • Clear
  • Contains
  • CopyTo
  • Remove

There is also a generic class named ReadOnlyCollection<T> which derives from the ICollection<T> defined in the BCL. As you can guess from the name of the ReadOnlyCollection, it implements a collection which cannot be changed after initialization. At the same time, ReadOnlyCollection<T> derives from the ICollection<T> what means that it should support all the operations defined in that parent generic interface. As a consequence, the ReadOnlyCollection has to implement Add, Clear and Remove methods somehow. But how it is supposed to implement them, taking into account that these operations are deprecated to perform on a read-only collection? Of course, there is no a meaningful way to solve this problem. So, the ReadOnlyCollection just throws the NotSupportedExpcetion from that modification methods. It clearly violates the Liskov Substitution Principle, since clients that work with ICollection<T> don’t expect such a behavior. And this is logical because other implementations of ICollection<T> such as List<T> don’t throw NotSupportedException, they just work as expected from a class which exposes modification methods. Throwing NotSupportedException is a strong smell of LSP violation.

Downcasts as a smell of LSP Violation

Another smell which indicates the LSP violation demonstrates itself by appearing of too many downcasts in the code base. If a client is constantly checking the actual types of base classes or interfaces, it automatically means, that a client is concerned about different implementers for some reason. It means that a client knows some internal details about the object’s hierarchy construction.This smell is a consequence of the LSP violation. An example of such a smell you’ve seen in the lecture where we discussed the relationships between square and rectangle.

Downcasts are not always the indication of LSP violations. It is allowed to downcast a type if you’re absolutely sure about the type you need to downcast to. Consider the following example:

public void PayCashback(int customerId, decimal amount)
{
    Customer c = repository.GetCustomer(customerId);
    var pc = c as PrivelegedCustomer;
    if (pc != null)
    {
        pc.AddMoneyToAccount(amount);
    }
    else
    {
        throw new ArgumentException("PayCashback on a regular customer!");
    }
}

For example, the client only calls this function with an id of a privileged customer. A client knows for sure that it passes the id of a privileged customer. We can freely downcast the object returned from the repository and perform an operation. This kind of downcast just, so to speak, reminds the compiler that the real type is PrivelegedCustomer.

Sum Up

Let’s sum up what common smells we may face with which indicate the LSP violation.

First of all, if a method throws the NotSupportedException from its body. Another notion which describes this case and which we haven’t discussed yet, but I like it very much is the notion of so-called “Refused Bequest”. So the meaning here is that the child class inherits something but it doesn’t now what to do with the inherited bequest and refuses it by throwing the NotSupportedException.

Another form of the same “Refused Bequest” is when a method has an empty implementation. This is the same case, the only difference is that, a method refuses bequest not so vehemently. Another smell concerns downcasts. Downcasts mean that a client has to know internal details of the abstraction it depends on. Here are some tips to conform with LSP:

  • Try to keep in your mind the “Tell, Don’t Ask” principle which means in essence that the client’s code should be able to just pass messages without thinking of any internal details of the collaborator. Ideally, the client should not be bothered by checking any conditions.
  • Remember that LSP violation is often a consequence of OCP or ISP violation. We will talk about the Interface Segregation Principle in the next section, so just take into account for now that fact. In such cases, fixing the root cause leads to an automatic fix of the LSP violation.
  • If you have two classes that share some logic and they are not substitutable, then think of creating a new base class for that two classes. Then inherit those two classes from a base class and ensure that they are substitutable with the new base class.

Conclusion

A simple definition sounds like this: “The Liskov Substitution Principle states that Subtypes must be substitutable for their base types.”
It means that a client shouldn’t experience different behavior using different inheritors of the same base class or an interface. So, the code which is compliant to the LSP enables proper use of polymorphism and such code becomes more maintainable.
There are two general rules you should adhere to keep your code compliant to the LSP:

  • Do not violate a Contract by either strengthening preconditions or weakening postconditions;
  • Do not violate Covariance\Contravariance, explicitly mark the generic parameters by “in” or “out” keyword if possible;

In practice, the LSP violation also demonstrates itself in the following ways:

  1. implementer throws NotSupportedException from a method which is inherited either from a base class or an interface.
  2. implementer has empty implementations, or stubs instead of meaningful implementation.
  3. switch-case statements appear here and there with checks of the actual type of an argument. Downcasts is a similar smell.

Want to take “SOLID Principles Explained” video course? Take it right now just for 10.99$.

Series Navigation<< SOLID Principles: The Open/Closed Principle (OCP)SOLID Principles: Interface Segregation Principle (ISP) >>
By |2018-12-27T19:05:21+00:00December 26th, 2018|.NET, Best Practices, C#, Design|0 Comments

About the Author:

I'm thankful enough for that I love what I do. I began my career as a postgraduate student participating in Microsoft ImagineCup contest. I've been working with .NET platform since 2003. I've been professionally architecting and implementing software for nearly 7 years, primarily based on .NET platform. I'm passionate about building rich and powerful applications using modern technologies. I'm a certified specialist in Windows Applications and Service Communication Applications by Microsoft. "If it's work, we try to do less. If it's art, we try to do more." - Seth Godin. What I can say is that software is my art.