The Liskov Substitution Principle states that a subtype should be substitutable for that type (without altering the correctness of the program).
I've read about the square/rectangle example, but I think that an example with vehicles will give me a better understanding of the concept.
Ostrich is a bird, but it can't fly, Ostrich class is a subtype of class Bird, but it shouldn't be able to use the fly method, that means we are breaking the LSP principle.
A very common violation of this principle is the partial implementation of interfaces or base class functionality, leaving unimplemented methods or properties to throw an exception (e.g. NotImplementedException).
SOLID Design Principles Explained: The Liskov Substitution Principle with Code Examples. The Open/Closed Principle, which I explained in a previous article, is one of the key concepts in OOP that enables you to write robust, maintainable and reusable software components.
The Liskov Substitution Principle (LSP) is strongly related to subtyping polymorphism. Based on subtyping polymorphism in an object-oriented language, a derived object can be substituted with its parent type. For example, if we have a Car object, it can be used in the code as a Vehicle .
For me, this 1996 Quote from Uncle Bob (Robert C Martin) summarises the LSP best:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
In recent times, as an alternative to inheritance abstractions based on sub-classing from a (usually abstract) base/super class, we also often use interfaces for polymorphic abstraction. The LSP has implications to both the consumer, and implementation of the abstraction:
LSP Compliance
Here is an example using an interface IVehicle
which can have multiple implementations (alternatively, you can substitute the interface for an abstract base class with several subclasses - same effect).
interface IVehicle { void Drive(int miles); void FillUpWithFuel(); int FuelRemaining {get; } // C# syntax for a readable property }
This implementation of a consumer of IVehicle
stays within the bounds of LSP:
void MethodWhichUsesIVehicle(IVehicle aVehicle) { ... // Knows only about the interface. Any IVehicle is supported aVehicle.Drive(50); }
Glaring Violation - Runtime type switching
Here's an example of a violation of LSP, using RTTI and then Downcasting - Uncle Bob calls this a 'glaring violation':
void MethodWhichViolatesLSP(IVehicle aVehicle) { if (aVehicle is Car) { var car = aVehicle as Car; // Do something special for car - this method is not on the IVehicle interface car.ChangeGear(); } // etc. }
The violating method goes beyond the contracted IVehicle
interface and hacks a specific path for a known implementation of the interface (or a subclass, if using inheritance instead of interfaces). Uncle Bob also explains that LSP violations using type-switching behaviour usually also violate the Open and Closed principle as well, since continual modification to the function will be required in order to accomodate new subclasses.
Violation - Pre condition is strengthened by a subtype
Another violation example would be where a "pre condition is strengthened by a subtype":
public abstract class Vehicle { public virtual void Drive(int miles) { Assert(miles > 0 && miles < 300); // Consumers see this as the contract } } public class Scooter : Vehicle { public override void Drive(int miles) { Assert(miles > 0 && miles < 50); // ** Violation base.Drive(miles); } }
Here, the Scooter subclass attempts to Violate the LSP as it tries to strengthen (further constrain) the precondition on the base class Drive
method that miles < 300
, to now a maximum of less than 50 miles. This is invalid, since by the contract definition of Vehicle
allows 300 miles.
Similarly, Post Conditions may not be weakened (i.e. relaxed) by a subtype.
(Users of Code Contracts in C# will note that preconditions and postconditions MUST be placed on the interface via a ContractClassFor
class, and cannot be placed within implementation classes, thus avoiding the violation)
Subtle Violation - Abuse of an interface implementation by a subclass
A more subtle
violation (also Uncle Bob's terminology) can be shown with a dubious derived class which implements the interface:
class ToyCar : IVehicle { public void Drive(int miles) { /* Show flashy lights, make random sounds */ } public void FillUpWithFuel() {/* Again, more silly lights and noises*/} public int FuelRemaining {get {return 0;}} }
Here, irrespective of how far the ToyCar
is driven, the fuel remaining will always be zero, which will be surprising to users of the IVehicle
interface (i.e. infinite MPG consumption - perpetual motion?). In this case, the problem is that despite ToyCar
having implemented all of the requirements of the interface, ToyCar
just inherently isn't a real IVehicle
and just "rubber stamps" the interface.
One way to to prevent your interfaces or abstract base classes from being abused in this way is to ensure a good set of Unit Tests are made available on the interface / abstract base class to test that all implementations meet the expectations (and any assumptions). Unit tests are also great at documenting typical usage. e.g. this NUnit Theory
will reject ToyCar
from making it into your production code base:
[Theory] void EnsureThatIVehicleConsumesFuelWhenDriven(IVehicle vehicle) { vehicle.FillUpWithFuel(); Assert.IsTrue(vehicle.FuelRemaining > 0); int fuelBeforeDrive = vehicle.FuelRemaining; vehicle.Drive(20); // Fuel consumption is expected. Assert.IsTrue(vehicle.FuelRemaining < fuelBeforeDrive); }
Edit, Re: OpenDoor
Opening doors sounds like a different concern entirely, so needs to be separated accordingly (i.e. the "S" and "I" in SOLID), e.g.
Add a separate interface
IDoor
, and then vehicles likeCar
andTruck
would implement bothIVehicle
andIDoor
interfaces, butScooter
andMotorcycle
would only implementIVehicle
.
In all cases, to avoid violating LSP, code which required objects of these interfaces should not downcast the interface to access extra functionality. The code should select the appropriate minimum interface / (super)class it needs, and stick to just the contracted functionality on that interface.
Image I want to rent a car when I'm moving house. I ring up the hire company and ask them what models they have. They tell me though that I'll just be given the next car that comes available:
public class CarHireService { public Car hireCar() { return availableCarPool.getNextCar(); } }
But they have given me a brochure that tells me all of their models come with these features:
public interface Car { public void drive(); public void playRadio(); public void addLuggage(); }
That sounds just what I'm looking for, so I book a car & go away happy. On moving day, a Formula One car shows up outside my house:
public class FormulaOneCar implements Car { public void drive() { //Code to make it go super fast } public void addLuggage() { throw new NotSupportedException("No room to carry luggage, sorry."); } public void playRadio() { throw new NotSupportedException("Too heavy, none included."); } }
I'm not happy, because I was essentially lied to by their brochure — it doesn't matter if the Formula One car has a fake boot that looks like it can hold luggage but won't open, that's useless for moving house!
If I'm told that "these are the things all of our cars do", then any car I'm given should behave in this way. If I can't trust the details in their brochure, it's useless. That's the essence of the Liskov Substitution Principle.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With