Introduction
Liskov Substitution Principle is one of five SOLID principles. It 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 that program[A1]. In other words - subclasses should be substitutable for their base classes[A2].
Violating LSP Will Surprise You
If software developers don’t comply with LSP, there’s a chance, bigger with each inheritance level, that produced objects will start to behave in an unexpected way. For instance, MakeASound
method of a Salmon
class will throw an exception even if salmon
s are animals and the abstract
animal can make a sound.
Another example may be a Drink
method executed with a Wine
parameter on instance of a Child
class. It’s expected that any Human
can drink any DrinkableFluid
, so if Child
derives from Human
and Wine
derives from Alcohol
which derives from DrinkableFluid
which derives from Fluid
– why – we ask ourselves - the Drink
method throws the exception?
How to Avoid Surprises with Code Contracts
Years ago, the Microsoft Research team started the Code Contracts project. It allows to define contracts in the form of preconditions, postconditions, and object invariants. It provides both static and runtime contract checking. Properly defined contracts can save time and effort by introducing an automatic analysis of usually omitted assumptions about form of the input data.
The most obvious and academic example of the unconscious assumption is division by zero:
double Divide(double a, double b)
{
return a / b;
}
A simple solution to this problem seems to be throwing an exception:
double Divide(double a, double b)
{
if (b == 0)
{
throw new ArgumentException("Divider can't be 0.");
}
return a / b;
}
Simple - yes, but not effective. The check will be executed in the runtime, so we’ll never be safe. We might catch the exception, present warning to the user and continue program execution, but in case of a complex system, it’s not really helpful.
With Code Contract, we can explicitly define our assumptions and if anywhere in our code they’ll be violated, we’ll be informed about it. Not in runtime only, but right after compiling process is finished.
double Divide(double a, double b)
{
Contract.Requires(b != 0);
return a / b;
}
If anywhere in our code we’ll try to execute Divide
method with b
equals zero, Code Contracts static
code analyzer will warn us:
CodeContracts: requires is false: b != 0
Code Contracts and LSP
Code Contracts can help us to detect LSP violations too.
Let’s define a bunch of classes:
public class Human
{
public int Age { get; set; }
public int ConsumedCalories { get; set; }
public Human(int age)
{
Contract.Requires(age > 0);
Contract.Requires(age < 130);
this.Age = age;
}
public virtual void Drink(DrinkableFluid fluid, int ml)
{
Contract.Requires(fluid != null);
Contract.Requires(ml > 0);
this.ConsumedCalories += Convert.ToInt32(ml * fluid.CaloriesPerMl);
}
}
public class Child : Human
{
public Child(int age)
: base(age)
{
Contract.Requires(age > 0);
Contract.Requires(age < 130);
this.Age = age;
}
public override void Drink(DrinkableFluid fluid, int ml)
{
Contract.Requires(!fluid.GetType().IsAssignableFrom(typeof(Alcohol)));
this.ConsumedCalories += Convert.ToInt32(ml * fluid.CaloriesPerMl);
}
}
public abstract class DrinkableFluid
{
public double CaloriesPerMl;
}
public class Sprite : DrinkableFluid
{
public Sprite()
{
this.CaloriesPerMl = 0.27;
}
}
public class Alcohol : DrinkableFluid
{
}
public class Wine : Alcohol
{
public Wine()
{
this.CaloriesPerMl = 0.85;
}
}
The problem with code above is poorly designed Human-Child inheritance. Both wine and sprite are drinkable fluids with no doubt. But wine can’t be consumed (sold, not consumed in fact, but let’s assume it can’t be consumed too in the ideal world) by children. If we have the classes Human
and Child
, we tend to think that Human
is an adult. Children aren’t subtype of adults obviously because they can’t substitute the adults. They can’t work, can’t drink alcohol and do many other things. Human
-Child
inheritance hierarchy is a poor abstraction.
Without Code Contracts, we’d just add a condition in the Drink
method of the Child
class and throw some exception:
public override void Drink(DrinkableFluid fluid, int ml)
{
if (fluid is Alcohol)
{
throw new ArgumentException("Children can't drink alcohol");
}
this.Calories += Convert.ToInt32(ml * fluid.CaloriesPerMl);
}
In the result, we’d be unaware of the problem until someone will execute the following code:
Human human= new Child(10);
DrinkableFluid fluid = new Wine();
human.Drink(fluid, 750);
But with Code Contracts, there’s no risk. Right after end of compilation, we’ll get a warning:
Conclusion
Code Contracts provide tools to explicitly define assumptions about method parameters and by static code analysis help to find bugs before they will appear in the runtime. The contracts can help you to avoid subtle issues like Liskov Substitution Principle violations.
Links
More information about Code Contracts can be found at:
Source code of the example:
References