Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Violating Liskov Substitution Principle (LSP)

5.00/5 (7 votes)
6 Sep 2013CPOL6 min read 53.1K  
How to not damage yourself when using inheritance.

Introduction

The article is about violating Liskov Substitution Principle (LSP).

It aims to show:

  • An example of violating Liskov Substitution Principle
  • Arising issues
  • Proposed solution

Background

Liskov Substitution Principle and subtyping

Simply put: Liskov Substitution Principle lies in the fact that if we have a bottle for liquid, we are able to pour into it water, milk, cola or acid and we don't expect that the bottle will explode.

The formal definition of Liskov Substitution Principle states that:

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.

The definition of subtyping itself sounds very similar:

If S is a subtype of T, then any term of type S can be safely used in a context where a term of type T is expected.

The key difference between these two lies in words desirable, safely. Liskov Substitution Principle should be considered more constricting than subtyping. So basically it is all about type safety and the whole spectrum from weak typing to strong typing where LSP is very rightist.

Liskov Subtitution Principle and type safety

Type safety is like a security control on an airport. Guards will not pass a person, who is potentially dangerous. It doesn't mean, that the person is really willing to destroy something, but taking into account rules, there is a considerable risk. In case of strong type safety and Liskov Substitution Principle, the rules are very constricting.

Computer languages differs in how they forces type safety. In practice: The stronger typing, the more compile errors during code writing. The weaker typing, the more runtime exceptions during program execution.

In certain circumstances, the programmer can follow programming principles in order to enhance type safety in his application, beyond computer language syntax rules.

Violating Liskov Substitution Principle in OOP

C# supports Object Oriented Programming style. OOP in turn, supports inheritance and polymorphism. It means, that we can compile the following lines:

C#
class MyCollection
{
    public int Count { get; set; }
}

class MyArray : MyCollection
{
}

class Program
{
    static void Main()
    {
        MyCollection collection = new MyArray();
    }
}

It seems to be desirable enabling to use MyCollection in place of MyArray since they both have a Count property. However notice, that Count of MyArray is immutable, since arrays have fixed number of elements. But it is valid. Subtypes can provide more warranties.

Now, we decided that collection should allow adding new elements:

C#
class MyCollection
{
    public int Count { get; private set; }
    public virtual void AddItem(int item) { /*...*/ }
}

class MyArray : MyCollection
{
    public override void AddItem(int item)
    {
        throw new System.NotSupportedException();
    }
}

class Program
{
    static void Main()
    {
        MyCollection collection = new MyArray();
        collection.AddItem(0); // <-- Bum!
    }
} 

That is how we have violated Liskov Substitution Principle. The code still compiles, but since we are able to cast down MyArray into MyCollection, we are also capable of adding a new item into a fixed sized array and that is incorrect. So here is an issue that arises when violating Liskov Substitution Principle. In such cases we have to throw a runtime exception.

An interesting fact is, that .NET Array also violates LSP while it derives from ICollection<T>:

C#
class Program
{
    static void Main()
    {
        ICollection<int> collection = new int[] {0};
        collection.Add(1); // <-- Throws NotSupportedException
    }
} 

The Add method is not visible as Array member since it is implemented explicitly. But we are always capable of casting an Array to ICollection<T> and operate on its instance from underlying interface level.

We could also violate Liskov Substitution Principle in different way, by constricting method parameters in derived class. Below is an example. Suppose that there are further types:

  • MyAlmostPositiveInts - allows to add only integers that are greater than or equal -5;
  • MyAlmostNegativeInts - allows to add only integers that are less than or equal 5;
  • MyOvelappingInts - allows to add only integers that are inclusively between -10 and 10;

C#
class MyCollection
{
    public int Count { get; private set; }
    public virtual void AddItem(int item) { /*...*/ }
}

class MyArray : MyCollection
{
    public override void AddItem(int item)
    {
        throw new System.NotSupportedException();
    }
}

class MyAlmostPositiveInts : MyCollection
{
    public override void AddItem(int item)
    {
        if (item < -5)
            throw new System.ArgumentException();
    }
}

class MyAlmostNegativeInts : MyCollection
{
    public override void AddItem(int item)
    {
        if (item > 5)
            throw new System.ArgumentException();
    }
}

class MyOvelappingInts : MyCollection
{
    public override void AddItem(int item)
    {
        if (item < -10 || item > 10)
            throw new System.ArgumentException();
    }
} 

In general: If you need to add some restriction in an overridden method and that restriction doesn't exist in baseline implementation, you probably violates Liskov Substitution Principle.

Solution

Since the previous chapter, we are able to write the following code:

C#
class Program
{
    static void Main()
    {
        MyCollection collection = new MyCollection();
        MyCollection array = new MyArray();
        MyCollection almostPositiveInts = new MyAlmostPositiveInts();
        MyCollection almostNegativeInts = new MyAlmostNegativeInts();
        MyCollection overlappingInts = new MyOvelappingInts();
 
        collection.AddItem(-1); // <-- OK
        collection.AddItem(0);  // <-- OK
        collection.AddItem(1);  // <-- OK
 
        array.AddItem(-1); // <-- NotSupportedException
        array.AddItem(0);  // <-- NotSupportedException
        array.AddItem(1);  // <-- NotSupportedException
 
        almostPositiveInts.AddItem(-15);  // <-- ArgumentException    
        almostPositiveInts.AddItem(-5);   // <-- OK
        almostPositiveInts.AddItem(0);    // <-- OK
        almostPositiveInts.AddItem(5);    // <-- OK 
        almostPositiveInts.AddItem(15);   // <-- OK  
 
        almostNegativeInts.AddItem(-15);  // <-- OK    
        almostNegativeInts.AddItem(-5);   // <-- OK 
        almostNegativeInts.AddItem(0);    // <-- OK
        almostNegativeInts.AddItem(5);    // <-- OK
        almostNegativeInts.AddItem(15);   // <-- ArgumentException  
 
        overlappingInts.AddItem(-15);  // <-- ArgumentException  
        overlappingInts.AddItem(-5);   // <-- OK 
        overlappingInts.AddItem(0);    // <-- OK 
        overlappingInts.AddItem(5);    // <-- OK 
        overlappingInts.AddItem(15);   // <-- ArgumentException   
    } 
}       

The solution should replace runtime exceptions with compile time errors or at least warnings.

In case of MyArray, we could simply extract a base class containing the Count property:

C#
class MyCollectionBase  
{ 
    public int Count { get; private set; } 
}
 
class MyCollection : MyCollectionBase 
{ 
    public virtual void AddItem(int item) { /*...*/ } 
}
 
class MyArray : MyCollectionBase 
{ 
}
 
class Program 
{ 
    static void Main() 
    {  
        MyCollection collection = new MyCollection(); 
        MyCollectionBase array = new MyArray();
 
        collection.AddItem(-1); // <-- OK 
        collection.AddItem(0);  // <-- OK 
        collection.AddItem(1);  // <-- OK
 
        array.AddItem(-1); // <-- Compiler error 
        array.AddItem(0);  // <-- Compiler error 
        array.AddItem(1);  // <-- Compiler error 
    } 
}    

In case of specific integers collection, the first step is the same. We have to disallow to add a new item from the base class level, because the base class doesn't provide user with any restrictions about the range of an integer being added. Therefore if the user receives a reference to MyCollection he assumes, that he is allowed to insert there any integer and currently it is not always the truth (i.e. MyPositiveInts could be assigned to a parameter of type MyCollection). Thus specific integers collection should also derive from MyCollectionBase that contains only Count.

After amendments, we have:

C#
class MyCollectionBase
{
    public int Count { get; private set; }
}
 
class MyCollection : MyCollectionBase
{
    public void AddItem(int item) { /*...*/ }
}
 
class MyArray : MyCollectionBase
{
}
 
class MyAlmostPositiveInts : MyCollectionBase
{
    public void AddItem(int item)
    {
        if (item < 5)
            throw new System.ArgumentException();
    }
}
 
class MyAlmostNegativeInts : MyCollectionBase
{
    public void AddItem(int item)
    {
        if (item > -5)
            throw new System.ArgumentException();
    }
}
 
class MyOvelappingInts : MyCollectionBase
{
    public void AddItem(int item)
    {
        if (item < -10 || item > 10)
            throw new System.ArgumentException();
    }
}
 
class Program
{ 
    static void Main()
    {
        MyCollection collection = new MyCollection();
        MyCollectionBase array = new MyArray();
        MyCollectionBase almostPositiveInts = new MyAlmostPositiveInts();
        MyCollectionBase almostNegativeInts = new MyAlmostNegativeInts();
        MyCollectionBase overlappingInts = new MyOvelappingInts();
 
        collection.AddItem(-1); // <-- OK
        collection.AddItem(0);  // <-- OK
        collection.AddItem(1);  // <-- OK
 
        array.AddItem(-1); // <-- Compiler error
        array.AddItem(0);  // <-- Compiler error
        array.AddItem(1);  // <-- Compiler error 
 
        almostPositiveInts.AddItem(-15);  // <-- Compiler error 
        almostPositiveInts.AddItem(-5);   // <-- Compiler error 
        almostPositiveInts.AddItem(0);    // <-- Compiler error 
        almostPositiveInts.AddItem(5);    // <-- Compiler error 
        almostPositiveInts.AddItem(15);   // <-- Compiler error 
 
        almostNegativeInts.AddItem(-15);  // <-- Compiler error 
        almostNegativeInts.AddItem(-5);   // <-- Compiler error 
        almostNegativeInts.AddItem(0);    // <-- Compiler error 
        almostNegativeInts.AddItem(5);    // <-- Compiler error 
        almostNegativeInts.AddItem(15);   // <-- Compiler error 
 
        overlappingInts.AddItem(-15);  // <-- Compiler error 
        overlappingInts.AddItem(-5);   // <-- Compiler error 
        overlappingInts.AddItem(0);    // <-- Compiler error 
        overlappingInts.AddItem(5);    // <-- Compiler error 
        overlappingInts.AddItem(15);   // <-- Compiler error 
    }
}  

From that point on, if we work with MyCollectionBase reference, we have to cast it to specific subtype in order to be able to add a new item. Thus we just can't violate Liskov Substitution Principle:

C#
class Program
{
    static void Main()
    {
        MyCollectionBase collectionBaseAlmostPositiveInts = new MyAlmostPositiveInts();
        MyCollectionBase collectionBaseAlmostNegativeInts = new MyAlmostNegativeInts();
        MyCollectionBase collectionBaseOvelappingInts = new MyOvelappingInts();
 
        MyAlmostPositiveInts almostPositiveInts =
              (MyAlmostPositiveInts)collectionBaseAlmostPositiveInts; 
        almostPositiveInts.AddItem(-15);  // <-- ArgumentException 
        almostPositiveInts.AddItem(-5);   // <-- OK 
        almostPositiveInts.AddItem(0);    // <-- OK 
        almostPositiveInts.AddItem(5);    // <-- OK 
        almostPositiveInts.AddItem(15);   // <-- OK 
 
        MyAlmostNegativeInts almostNegativeInts =
              (MyAlmostNegativeInts)collectionBaseAlmostNegativeInts;  
        almostNegativeInts.AddItem(-15);  // <-- OK 
        almostNegativeInts.AddItem(-5);   // <-- OK 
        almostNegativeInts.AddItem(0);    // <-- OK 
        almostNegativeInts.AddItem(5);    // <-- OK 
        almostNegativeInts.AddItem(15);   // <-- ArgumentException 
 
        MyOverlappingInts overlappingInts =
              (MyOverlappingInts)collectionBaseOverlappingInts;  
        overlappingInts.AddItem(-15);  // <-- ArgumentException  
        overlappingInts.AddItem(-5);   // <-- OK  
        overlappingInts.AddItem(0);    // <-- OK  
        overlappingInts.AddItem(5);    // <-- OK  
        overlappingInts.AddItem(15);   // <-- ArgumentException  
    }
}    

If we think of the current situation, we will notice that we have to check the actual type of MyCollectionBase each time we want to add an item. In practice that would be very inconvenient. There are also still runtime ArgumentException occurrences. However, they not result from violating Liskov Substitution Principle. They are there because the compiler doesn't know how to read our if statements in Add method and not because we reinforced limitations in derived class. Anyway we will deal with that issue later.

How to obtain access to Add method in safe way without explicit casting? To achieve that, integers collections must be derived from same base class or implement same interface that contains Add. Unfortunately that was already and was unsafe due to Liskov Substitution Principle violation. Though, we are still able to extract common base class to certain extent. Notice that integers collection have intersections. We can utilize that:

C#
class MyCollectionBase
{
    public int Count { get; private set; }
}
 
class MySafeIntsBase : MyCollectionBase
{
    public virtual void AddItem(int item)
    {
        if (item < -5 || item > 5)
            throw new System.ArgumentException();
    }
}
 
class MyPositiveIntsSafeBase : MySafeIntsBase
{
    public override void AddItem(int item)
    {
        if (item < -5 || item > 10)
            throw new System.ArgumentException();
    }
}
 
class MyNegativeIntsSafeBase : MySafeIntsBase
{
    public override void AddItem(int item)
    {
        if (item < -10 || item > 5)
            throw new System.ArgumentException();
    }
}
 
class MyAlmostPositiveInts : MyPositiveIntsSafeBase
{
    public override void AddItem(int item)
    {
        if (item < -5)
            throw new System.ArgumentException();
    }
}
 
class MyAlmostNegativeInts : MyNegativeIntsSafeBase
{
    public override void AddItem(int item)
    {
        if (item > 5)
            throw new System.ArgumentException();
    }
}
 
class MyOvelappingInts : MySafeIntsBase
{
    public override void AddItem(int item)
    {
        if (item < -10 || item > 10)
            throw new System.ArgumentException();
    }
}

Each level of inheritance relaxes the restrictions regarding item being added. That is allowed. We only can't tightens the rules. Regrettably the compiler will not follow to our guidelines and we are still able to insert to our MySafeIntsBase collection any integer. Good point is, that our inheritance hierarchy and class names indicates our intentions.

MySafeIntsBase defines safe range as <-5;5> and thus can be used in places where we can expect the instance of MyAlmostPositiveInts<code><code> <-5; +∞), MyAlmostNegativeInts <-∞; 5) or MyOverlappingInts <-10;10>. It will give the developer a clue, that he should insert there only integers that are valid for all those types.

MyPositiveSafeIntsBase defines safe range as <-5;10> and thus can be used in places where we can expect the instance of MyAlmostPositiveInts or MyOverlappingInts.

MyNegativeSafeIntsBase defines safe range as <-10;5> and thus can be used in places where we can expect the instance of MyAlmostNegativeInts or MyOverlappingInts.

Notice, that theoretically we could derive MyOverlappingInts also from MyPositiveSafeIntsBase and MyNegativeSafeIntsBase since the union of sets <-10;5> and <-5;10> gives <-10;10>. We can build less constricting type from more constricting. But multiinheritance in C# is not allowed. We could achieve the goal by utilizing interfaces, but I will combine that step in next subsection which debates about Code Contracts.

Code Contracts

Since .NET Framework 4.0 there is an addition support for programming safety called Code Contracts. They in particular extend the static analysis capability of the compiler. In our case, they are able to show compilation warning if we try to put invalid integer value to MySafeIntsBase collection. I will not explain how to configure and use Code Contracts, but the below is a highly secure solution, that almost disallow violating Liskov Substitution Principle (almost, because Code Contracts generates compilation warnings, not errors):

C#
#region IMySafeIntsBase contract binding
 
[ContractClass(typeof(IMySafeIntsBaseContract))]
public partial interface IMySafeIntsBase
{
    void AddItem(int item);
}
 
[ContractClassFor(typeof(IMySafeIntsBase))]
abstract class IMySafeIntsBaseContract : IMySafeIntsBase
{
    public void AddItem(int item)
    {
        Contract.Requires<ArgumentException>(
            item >= -5 && item <= 5,
            "Value should be between <-5;5>.");
    }
}
 
#endregion
 
#region IMySafeIntsBase contract binding
 
[ContractClass(typeof(IMyPositiveIntsSafeBaseContract))]
public partial interface IMyPositiveIntsSafeBase
{
    void AddItem(int item);
}
 
[ContractClassFor(typeof(IMyPositiveIntsSafeBase))]
abstract class IMyPositiveIntsSafeBaseContract : IMyPositiveIntsSafeBase
{
    public void AddItem(int item)
    {
        Contract.Requires<ArgumentException>(
            item >= -5 && item <= 10,
            "Value should be between <-5;10>.");
    }
}
 
#endregion
 
#region IMyNegativeIntsSafeBase contract binding
 
[ContractClass(typeof(IMyNegativeIntsSafeBaseContract))]
public partial interface IMyNegativeIntsSafeBase
{
    void AddItem(int item);
}
 
[ContractClassFor(typeof(IMyNegativeIntsSafeBase))]
abstract class IMyNegativeIntsSafeBaseContract : IMyNegativeIntsSafeBase
{
    public void AddItem(int item)
    {
        Contract.Requires<ArgumentException>(
            item >= -10 && item <= 5,
            "Value should be between <-10;5>.");
    }
}
 
#endregion
 
#region IMyAlmostPositiveInts contract binding
 
[ContractClass(typeof(IMyAlmostPositiveIntsContract))]
public partial interface IMyAlmostPositiveInts
{
    void AddItem(int item);
}
 
[ContractClassFor(typeof(IMyAlmostPositiveInts))]
abstract class IMyAlmostPositiveIntsContract : IMyAlmostPositiveInts
{
    public void AddItem(int item)
    {
        Contract.Requires<ArgumentException>(
            item >= -5,
            "Value should be greater than or equal -5.");
    }
}
 
#endregion
 
#region IMyAlmostNegativeInts contract binding
 
[ContractClass(typeof(IMyAlmostNegativeIntsContract))]
public partial interface IMyAlmostNegativeInts
{
    void AddItem(int item);
}
 
[ContractClassFor(typeof(IMyAlmostNegativeInts))]
abstract class IMyAlmostNegativeIntsContract : IMyAlmostNegativeInts
{
    public void AddItem(int item)
    {
        Contract.Requires<ArgumentException>(
            item <= 5,
            "Value should be less than or equal 5.");
    }
}
 
#endregion
 
#region IMyOvelappingInts contract binding
 
[ContractClass(typeof(IMyOvelappingIntsContract))]
public partial interface IMyOvelappingInts
{
    void AddItem(int item);
}
 
[ContractClassFor(typeof(IMyOvelappingInts))]
abstract class IMyOvelappingIntsContract : IMyOvelappingInts
{
    public void AddItem(int item)
    {
        Contract.Requires<ArgumentException>(
            item >= -10 && item <= 10,
            "Value should be between <-10;10>.");
    }
}
 
#endregion
 
class MyCollectionBase
{
    public int Count { get; private set; }
}
 
class MyAlmostPositiveInts : MyCollectionBase, IMyAlmostPositiveInts, IMyPositiveIntsSafeBase, IMySafeIntsBase
{
    private void AddItem(int item) { }
    void IMyAlmostPositiveInts.AddItem(int item) { AddItem(item); }
    void IMyPositiveIntsSafeBase.AddItem(int item) { AddItem(item); }
    void IMySafeIntsBase.AddItem(int item) { AddItem(item); }
}
 
class MyAlmostNegativeInts : MyCollectionBase, IMyAlmostNegativeInts, 
                             IMyNegativeIntsSafeBase, IMySafeIntsBase
{
    private void AddItem(int item) { }
    void IMyAlmostNegativeInts.AddItem(int item) { AddItem(item); }
    void IMyNegativeIntsSafeBase.AddItem(int item) { AddItem(item); }
    void IMySafeIntsBase.AddItem(int item) { AddItem(item); }
}
 
class MyOvelappingInts : MyCollectionBase, IMyOvelappingInts, 
      IMyPositiveIntsSafeBase, IMyNegativeIntsSafeBase, IMySafeIntsBase
{
    private void AddItem(int item) { }
    void IMyOvelappingInts.AddItem(int item) { AddItem(item); }
    void IMyPositiveIntsSafeBase.AddItem(int item) { AddItem(item); }
    void IMyNegativeIntsSafeBase.AddItem(int item) { AddItem(item); }
    void IMySafeIntsBase.AddItem(int item) { AddItem(item); }
}
 
class Program
{
    static void Main()
    {
        IMySafeIntsBase safe = new MyOvelappingInts();
        safe.AddItem(-15); // <-- Warning, CodeContracts: requires is false:
                           //     item >= -5 && item <= 5 (Value should be between <-5;5>.)
        safe.AddItem(-10); // <-- Warning, CodeContracts: requires is false:
                           //     item >= -5 && item <= 5 (Value should be between <-5;5>.)
        safe.AddItem(-5);  // <-- OK
        safe.AddItem(0);   // <-- OK
        safe.AddItem(5);   // <-- OK
        safe.AddItem(10);  // <-- Warning, CodeContracts: requires is false:
                           //     item >= -5 && item <= 5 (Value should be between <-5;5>.)
        safe.AddItem(15);  // <-- Warning, CodeContracts: requires is false:
                           //     item >= -5 && item <= 5 (Value should be between <-5;5>.)
 
        IMyPositiveIntsSafeBase positiveSafe = (IMyPositiveIntsSafeBase)safe;
        positiveSafe.AddItem(-15); // <-- Warning, CodeContracts: requires is false:
                                   //     item >= -5 && item <= 10
                                   //     (Value should be between <-5;10>.)
        positiveSafe.AddItem(-10); // <-- Warning, CodeContracts: requires is false:
                                   //     item >= -5 && item <= 10
                                   //     (Value should be between <-5;10>.)
        positiveSafe.AddItem(-5);  // <-- OK
        positiveSafe.AddItem(0);   // <-- OK
        positiveSafe.AddItem(5);   // <-- OK
        positiveSafe.AddItem(10);  // <-- OK
        positiveSafe.AddItem(15);  // <-- Warning, CodeContracts: requires is false:
                                   //     item >= -5 && item <= 10
                                   //     (Value should be between <-5;10>.)
 
        IMyNegativeIntsSafeBase negativeSafe = (IMyNegativeIntsSafeBase)safe;
        negativeSafe.AddItem(-15); // <-- Warning, CodeContracts: requires is false:
                                   //     item >= -10 && item <= 5
                                   //     (Value should be between <-10;5>.)
        negativeSafe.AddItem(-10); // <-- OK
        negativeSafe.AddItem(-5);  // <-- OK
        negativeSafe.AddItem(0);   // <-- OK
        negativeSafe.AddItem(5);   // <-- OK
        negativeSafe.AddItem(10);  // <-- Warning, CodeContracts: requires is false:
                                   //     item >= -10 && item <= 5
                                   //     (Value should be between <-10;5>.)
        negativeSafe.AddItem(15);  // <-- Warning, CodeContracts: requires is false:
                                   //     item >= -10 && item <= 5
                                   //     (Value should be between <-10;5>.)
 
        IMyOvelappingInts overlaping = (IMyOvelappingInts)safe;
        overlaping.AddItem(-15); // <-- Warning, CodeContracts: requires is false:
                                 //     item >= -10 && item <= 10
                                 //     (Value should be between <-10;10>.)
        overlaping.AddItem(-10); // <-- OK
        overlaping.AddItem(-5);  // <-- OK
        overlaping.AddItem(0);   // <-- OK
        overlaping.AddItem(5);   // <-- OK
        overlaping.AddItem(10);  // <-- OK
        overlaping.AddItem(15);  // <-- Warning, CodeContracts: requires is false:
                                 //     item >= -10 && item <= 10
                                 //     (Value should be between <-10;10>.)
    }
}

Summary

Writing bulletproof code always requires a lot of effort. The larger project, the more type safety should be applied in order to keep everything easy to maintain. In this article I've tried to explain what is the Liskov Substitution Principle, how to violate it by inappropriate inheritance hierarchy and shown the possible solution.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)