The Liskov substitution principle (LSP, the L in SOLID) relates a type and its subtypes using the following definition:
What is wanted here is something like the following substitution property: 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.
Which is often interpreted as “at any point in a program where I use an object of type T, I can safely use an object of type S”. In this context, safely means the program can use S without it crashing or causing otherwise undesirable behavior. This interpretation, however, is less stringent than Liskov’s definition, which says: “the behavior of P is unchanged when o1 is substituted for o2”. A behaviorially unchanged program to me means there’s no functional difference, or no difference in output/side-effects (this does allow for differences in space/time requirements).
To give an example of objects that do conform to LSP, let’s look at an abstract data type (ADT), such as a set or a stack. Obviously, there are many ways of implementing a set. However, from the point of view of a program using a set, it shouldn’t matter which implementation you use for the program to behave correctly. If the set implementation attains this property, it has conformed to LSP.
When doing OOP, however, changing the behavior of the program is often exactly what you want. Take the ubiquitous UI Widget class hierarchy example. In this case, you subclass a base Widget in order to get the information specific to your application onto the screen. Here, different behavior for the program using the Widget classes is the entire goal of this design. This means though, that with regard to LSP, the subclass is not a subtype of the base Widget, nor should it be. The subtle point here is that there’s a difference between subtypes and subclasses. A subtype is defined by LSP, but it says nothing about sublasses. However, when talking about LSP in SOLID, LSP is often explained in terms of subclasses instead of subtypes, and this is wrong as this example illustrates (unless you think the design is actually bad).
LSP, therefore, is not a general principle to live by when doing OOP. In fact, a lot of programs/designs are useful exactly because they violate it. OOP is much more powerful than just implementing ADTs, and we shouldn’t apply constraints that hold for ADTs onto objects. That being said, you should make sure that you don’t violate any implicit or explicit assumptions made by the clients of the base class, which I assume is the underlying reason LSP made it into the SOLID principles at all.
Credits
I already had these concerns when first reading about LSP a couple of years ago, however I only thought about writing a blog post about it when I watched a presentation by Kevlin Henney (at around 33:00) a couple of weeks ago where he talked about the same subject. Also, my thinking on objects vs ADTs is influenced greatly by “On Data Abstraction, Revisited” by William Cook and “The power of interoperability: Why objects are inevitably” by Jonathan Aldrich, both are excellent reads if you are interested in OOP theory.