This post is continuation of the SOLID principles series that I have been writing about on SOLID Principles.
Please make sure you read my other blogs on the topic
- S The Single Responsibility Principle
- O The Open/Closed Principle
- L The Liskov substitution principle
- I Interface Segregation
- D Dependency Injection
The Liskov substitution principle (LSP) is a set of rules for creating inheritance hierarchies in which the consumers of these classes can reliably use any class or subclass without breaking their code.
If S is subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program — Barbara Liskov
Let’s look at some practical examples to understand this further. Consider we have a base class called ShippingStrategy
and it is being inherited by WorldWideShippingStrategy
and it contains the following method
decimal CalculateShippingCost(
float packageWeightInKilograms,
Size<float> packageDimensionsInInches,
RegionInfo destination)
The only thing that this method shares between ShippingStrategy
and WorldWideShippingStrategy
is its signature. The implementation could be completely different between these two classes.
Let’s see the base class(ShippingStrategy
) implementation
public decimal CalculateShippingCost(
float packageWeightInKilograms,
Size<float> packageDimensionsInInches,
RegionInfo destination)
{
if(packageWeightInKilograms <= 0f)
throw new Exception($"{nameof(packageWeightInKilograms)} must be positive and non zero");
// Actual logic
return default(decimal)
}
As you can see from the above code example the function has a precondition to make sure the packageWeightInKilograms
is always positive and non zero. Preconditions are defined as all the conditions necessary for a method to run reliably and without fault.
Now let’s consider the WorldWideShippingStrategy
implementation
public decimal CalculateShippingCost(
float packageWeightInKilograms,
Size<float> packageDimensionsInInches,
RegionInfo destination)
{
if(packageWeightInKilograms <= 0f)
throw new Exception($"{nameof(packageWeightInKilograms)} must be positive and non zero");
if(destination == null)
throw new Exception($"{nameof(destination)}, Destination must be provided")
// Actual logic
return default(decimal)
}
Now since we have added an additional precondition in this implementation if any consumer using the base type might assume that they can pass null to the destination parameter and if they try to use the WorldWideShippingStrategy
their program would break.
This is the actual problem that LSP principle is trying to address using the following rules
Contract Rules
- Preconditions cannot be strengthened in a subtype
- Postconditions cannot be weakened in a subtype
- Invariants — conditions that must remain true through the lifetime of an object
Variance Rules
- There must be contravariance of the method arguments in the subtype.
- There must be covariance of the return types in the subtype
- No new exceptions can be thrown by the subtype unless they are part of the existing exception hierarchy
Conclusion
Even though LSP might appear to be one of the complex principles among SOLID principles, once we understand the concepts of preconditions, postconditions and variance it becomes easier to grasp.
Hope this helps!!
3 Comments