This is the 4th post on SOLID Code with Emergent Design.
Liskov Substitution Principle
"Derived classes must be usable through the base class interface without the need for the user to know the difference"
This principle is about following good techniques for inheritance. It says code that uses the base class should not need to have different logic based upon which concrete class is used. In other words, avoid logic in the calling code that looks something like "do this unless the concrete type is X, then do this other thing…"
As an example, imagine a drawing program that uses a base class of Shape. When the program wants to draw the shape, it simply calls Shape.draw(). It does not need to know if it’s drawing a square or a circle or a rhombohedron – they’re all just shapes:
Okay, makes sense – then what’s the problem?
The problem, it seems, is once again how we were taught to do "good" OO Design. In particular, that pesky Is A Relationship. You may be looking at the above diagram and thinking it’s a bit odd because it shows Squares and Rectangles at the same level even though we all know that a square Is A rectangle. Therefore, our OO inheritance rules tell us that Square should actually inherit from Rectangle.
Of course, a Square, like a Rectangle, has both a width and height. And each will need to know these dimensions to properly execute it’s draw() method. However, what happens if the caller does something like this?
s.setWidth( 2 );
s.setHeight( 10 );
s.draw( ) // oops!
Well, considering that a square requires an equal width and height, that’s not likely to draw an actual valid square, now is it?
So how do we get around this? One option would be to create a new method, setWidthHeight( ), that will set the width and the height together, thus ensuring that the square remains valid. But then, what does Square do inside it’s setWidth() and setHeight() methods that it has inherited from Rectangle? Well, maybe it just throws exceptions because those methods aren’t valid within Square.
Okay, well beyond things getting fairly stinky at this point, we’ve clearly broken our principle because now the calling code needs to know what type of Rectangle it’s dealing with and include logic to distinguish them, such as:
{
if( rect instanceof Square ){
setWidthHeight( x );
}else{
setWidth( x );
setHeight( y );
}
}
Right, so that’s not going to work. Well, what if we have Square implement the methods it’s implemented, but do it in such a way that whenever setWidth() or setHeight() is called, it goes ahead and sets both it’s width and height to the same value.
The problem is that we’ve still violated the LSP in that Square is no longer providing the same behavior as it’s base class of Rectangle. That is, it’s no longer quite doing what the base class methods are supposed to do. While calling setWidth() only sets the width in Rectangle, it sets both width and height in Square. While you and I might understand this, being the geometry gurus that we are, a caller that knew nothing about squares, only about rectangles, would quite rightly find this behavior invalid.
And, of course, the LSP says that the caller only has to understand the base class. They should not even have to know that subclasses exist, much less understand their intracies (think: abstract factories).
So, for example, the following calling code – which assumes that the caller doesn’t have to know anything about a concrete class beyond the base class it’s derived from – would break:
{
rect.setWidth( 2 );
rect.setHeight( 10 );
// This assertion will fail for Squares
assert((rect.getWidth() * rect.getHeight()) == 20);
}
Once again, returning to Uncle Bob’s wisdom:
"This leads us to a very important conclusion. A model, viewed in isolation, can not be meaningfully validated. The validity of a model can only be expressed in terms of its clients. Thus, when considering whether a particular design is appropriate or not, one must not simply view the solution in isolation. One must view it in terms of the reasonable assumptions that will be made by the users of that design."