Author: Brandon Pearman

The views expressed here are mine alone and do not reflect the view of my employer.

"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." - Barbara Liskov

Subtypes must be substitutive for their base types. eg dog and cat are subtypes of animal, so cat or dog should be able to replace animal without breaking the program.

NOTE: Contracts are not simple interfaces, they are more than that. See Design by contract (DbC) by Bertrand Meyer

Design by Contract vs LSP:

There are some points to design by contract, which should be considered for Liskovs substitution principle. Design by contract defines the use of Preconditions, Postconditions, Invariants and Exceptions.

Simple Contract Violations Example

Take a look at the below class:


public class BaseClass
{
    int Calculate(string id, int amount)
    {
        if(id == null)
        {
            throw new NullIdException();
        }
        Calculator calculator = GetCalculator(id);
        int positiveInt = calculator.GetPositiveInt(amount);
        return positiveInt;
    }
}
  

In this example:

  • string id: The id is used to get a specific calculator, and cannot be null otherwise a NullIdException is thrown.
  • int amount: There are no restrictions on amount, it can be positive or negative.
  • return int: The calculator always returns a positive value. meaning this method always returns a positive int.

BaseClass will be used throughout the code. ids are always passed in, negative and positive ints are passed in and other code handles NullIdException as well as uses the positive int it gets.

Months later a new DerivedClass is introduced into the system because new logic is required in some cases.

Take a look at the below class:


public class DerivedClass : BaseClass
{
    int Calculate(string id, int amount)
    {
        if(amount < 0)
        {
            throw new InvalidAmountException();
        }
        int anyInt = Calculate(amount);
        return anyInt;
    }
}
  
  • string id: Not used anymore and NullIdException is never thrown.
  • int amount: Is forced to be positive else it throws a new type of exception InvalidAmountException. (this is a strengthened precondition)
  • return int: Returns a negative or positive int. (this is a weakened postcondition)

Now consider how existing code will handle this new DerivedClass.

  • The fact that it does not use the id or throw NullIdException is not an immediate problem.
  • No code checks whether it passes negative ints to the method (could damage stuff), no code is prepared for an InvalidAmountException.
  • No code is prepared to handle negative values being returned to it.

All these issues may cause unattended effects or may break the program.

collapse
A Dot Net Framework violation

Spot whats off with the below Code:


  ICollection myCollection = new ReadOnlyCollection(new List() { "a" });
  myCollection.Add("b");
    

Did you spot it? We are trying to Add to a ReadOnlyCollection. Surly you can't modify a ReadOnlyCollection.

ReadOnlyCollection inherits from ICollection which has an Add method, but ReadOnlyCollection throws an exception if you attempt to add. Any code which uses ICollection.Add() will break if a ReadOnlyCollection is passed in.

collapse

Liskov's substitution principle imposes some standard requirements on signatures called covariance and contravariance. They have been adopted in newer object-oriented programming languages like C#. Covariance and Contravariance can be a little tricky to understand in full, I'm not going to cover it in full here but just a brief look as a reminder.

Covariance: allows a method to be assigned to a delegate, where the methods return type, is a class that is derived from the delegates return type.

Covariance Example

eg delegate return type is Animal, and the methods return type is dog.


                  private delegate Animal GetAnimalDelegateStructure();

                  public static void Main()
                  {
                      GetAnimalDelegateStructure getAnimalDelegate = GetAnimal;
                      Animal animal = getAnimalDelegate();
                  }

                  private static Rat GetAnimal()
                  {
                      return new Rat("Greg");
                  }
             

C# 4 allows a Covariance with the "out" keyword


    interface ITestIn // T is covariant
    {
        T Test();
        // void Test(T t); // Compile time exception: the out keyword only allows T to be a return type not a parameter
    }
    

The above interface can be used like this to align with Covariance


    private void Covariance()
    {
        // Can do this:
        ITestOut testAnimal = new TestOut();

        // Not this:
        ITestOut testAnimal = new TestOut();
    }
    
collapse

Contravariance: allows a method to be assigned to a delegate, when the methods parameter type, is a class that is derived from the delegates parameter type.

Contravariance Example

eg delegate parameter type is dog y, and the methods parameter type is Animal y.


  private delegate void PrintNameDelegateStructure(Rat rat);

  public void Main()
  {
      PrintNameDelegateStructure printNameDelegate = PrintAnimalName;
      printNameDelegate(new Rat("Greg"));
  }

  private void PrintAnimalName(Animal animal)
  {
      Console.WriteLine(animal.Name);
  }
  

C# 4 allows a Contravariance with the "in" keyword


  interface ITestIn // T is contravariant
  {
      void Test(T t);
      // T Test(); // Compile time exception: the "in" keyword only allows T to be a parameter and not a return type
  }
  

The above interface can be used like this to align with Contravariance


  private void Contravariance()
  {
      // Can do this:
      ITestIn validDog = new TestIn();
      validDog.Test(new Dog());

      // Not this:
      ITestIn invalidDog = new TestIn();
  }
  
collapse

Check out these links for more info:

My design and architecture repo