LSP was defined way back in 1988 by Dr. Barbara Liskov, who incidently won the 2009 Turing Award, perhaps the most prestigious award in computer science. Her original definition was:
“If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.”
But Robert Martin offered a much more terse definition:
What this is saying is that a derived class should honor the contracts made by it's parent classes. In other words, if a method signature accepts a base class reference, then it should be able to accept an instance of any class derived from that base class without affecting the functioning of the method.
Object-oriented programmers will be familiar with the concepts of abstraction and polymorphism. In statically-typed O-O languages, like C++, Java and C#, the key mechanism to achieve polymorphism is inheritance. LSP is a guiding principle that restricts how inheritance is used such that the OCP is not violated.
Technically speaking, the type of polymorphism this principle addresses is inclusion polymorphism. Inclusion polymorphism occurs in languages that allow subtypes and inheritance whereby an instance of a subtype can be manipulated by the same functions that operate on instances of the supertype (parent class type). This implies a reference (or pointer) of the parent class type can also refer to any child object, meaning that the type of the object being referred to must be determined at runtime. Since the type of object cannot be determined until runtime, and virtual methods are defined per type, it implies that a method call may be executed either in the parent or the child class and this dispatch decision cannot be made until runtime.
The Liskov Substitution Principle helps to guarantee inclusion polymorphism, which is a good thing because inclusion polymorphism improves reuse. All code that references the superclass can be reused referencing a subclass.
Why Follow It?
By adopting the LSP, the correctness of a method accepting base class references is guaranteed under certain substitutability conditions. Furthermore, since LSP is actually a special case of the Open-Closed Principle, every time you violate the LSP, you violate the OCP as a result - but not the other way round. It is this relationship between OCP and LSP that makes it easier to spot since developers tend to understand OCP much more readily that they do with LSP.
Say you develop a class hierarchy using inheritance, and you have a method in your base class that accepts a base class reference, and when you pass in a derived class reference you get unexpected results, it's a strong sign that the inheritance chain and the object model is incorrect. You need to remember that a class must fulfill an "is-a" relationship in order for it to be able to inherit from another class. This relationship is about behavior not data! In this sense, LSP is good at exposing faulty abstractions.
LSP is the reason that it is hard to design and create good deep hierarchies of sub classes and the reason to consider using composition over inheritance. (The strategy pattern is a prototypical example of the flexibility of composition over inheritance.)
The whole point of the Liskov Substitution Principle is really to make you think clearly about the expected behavior and expectations of a class before you derive new classes from it.
Common examples for violation of LSP are Rectangle::Square, Circle::Ellipse, etc. Rather than reproduce those here, take a look at the examples in Robert Martin's paper.
Design By Contract
The Liskov Substitution Principle is closely related to the design by contract methodology, which provides rules telling us the conditions under which it is acceptable to substitute a derived class for a base class:
- Preconditions cannot be strengthened in a subclass.
- Postconditions cannot be weakened in a subclass.
In other words, a sub-type can only have weaker pre-conditions and stronger post-conditions than its base class. Put differently...derived methods should expect no more and provide no less.
Signs of LSP violations include:
- A subclass that does not keep all the external observable behavior of it's parent class
- A subclass modifies, rather than extends, the external observable behavior of it's parent class.
- A subclass that throws exceptions in an effort to hide certain behavior defined in it's parent class
- A subclass that overrides a virtual method defined in it's parent class using an empty implementation in order to hide certain behavior defined in it's parent class
Method overriding in derived classes is probably the biggest cause of LSP violations. All method overrides should be done with great impunity as to avoid these violations.
In addition, the principle implies that no new exceptions should be thrown by methods of the subclass, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the superclass. (think: co-variance and contra-variance).
A function using a class hierarchy violating the principle uses a reference to a base class, yet must have knowledge of the subclasses. Such a function violates the open/closed principle because it must be modified whenever a new derivative of the base class is created, and that really sucks because the compiler or your existing unit tests won't find these cases for you - you have to become a UN weapons inspector, remember what things exactly you need to look for, and go hunt them down manually!
In the words of Robert Martin, Agile Principles, Patterns and Practices in C# (P.149):
"A good engineer learns when compromise is more profitable than perfection. However, conformance to LSP should not be surrendered lightly. The guarantee that a subclass will always work where its base classes are used is a powerful way to manage complexity. Once it is forsaken, we must consider each subclass individually."