SOLID Code with Emergent Design – The L

Hacker News LinkedIn Facebook Twitter by Abby Fichtner

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:

Class diagram.  The calling code invokes Shape, which has been implemented by Rectange, Square, and Circle

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 Class diagram.  Square inherits from Rectangle, which has seperate methods for setWidth() and setHeight()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?

Square s = new Square( );
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:

public void myMethod( Rectangle rect )
{
    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:

public void setDimensions( Rectangle rect )
{
    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."

  • Philippe

    The problem is that the domain model of geometry is immutable. You see whether something is a square or a rectangle based on its properties, but you can’t set them afterwards. In effect, if you make the classes into value objects like described below, the analogies with the domain model do hold. So you can basically keep all your inspecting methods (those that don’t change the object) and just change all the methods that do change the object into methods that return a new object instead. This actually goes for most of the mathematical domain.

    class Rectangle {

    setWidth(width): Rectangle { //returns a new rectangle with the same height and a new width}

    setHeight(height): Rectangle{ //returns a new rectangle with the same width and a new height}

    }

  • Frank

    It would comply with the LSP, in the sense that all methods of the Square API would have meaning for Rectangle as well. You could have getPerimeterLength(), getDiagonalLength(), getSurface() and so on. Of course Rectangle would have to override these methods’ implementation, but that is not an issue. The only possible problem is when a caller wants to rotate a square, and expects it to have more symmetries then a rectangle actually has.
    The real problem with the idea however is that customers (in this case mathematicians) and developers would be speaking a different language. For one, a rectangle is most definitely not always a square, for the other it is. That is a bad thing because both parties should have a common language when working in a team. The better solution is the second one I described. That one also respects the LSP, does not call for extra methods on Square (just changes the behaviour of the methods already in Rectangle) and respects what the users of the domain model would expect from the object model.

  • Abby Fichtner

    What would happen if Rectangle would extend Square instead?

    Interesting. If you had rectangle extend square, would that comply with the LSP? That is, would rectangle be usable through Square’s interface without the need for the user to know the difference?

  • Frank

    What would happen if Rectangle would extend Square instead?

    The problem here seems to be an inconsistency between the ideal domain model (in which every square is a rectangle but not every rectangle is a square) and the ideal object model (in which Rectangle actually has more functionality than Square – you can set the width but also the height).
    So by having Rectangle extend Square, you could only have a setWidth() method on Square, and add a setHeigth() method on Rectangle.
    This seems like a horrible thing to do, but imagine the classes would instead be called Blob and Blab with Blob needing foo() and Blab needing foo() and foo2(). You wouldn’t think twice about extending Blab from Blob.

    It would stop working though, when the program would require the shapes to rotate or mirror. Then Square would have more symmetries then Rectangle.

    In any case, as a matter of principle, the object model should respect the domain model. So the best solution of all, I think, would be to leave the model as it is, with Square extending from Rectangle, but to have both setHeight() and setWidth() “overrule” eachother when Square implements them:

    setHeight(int height){
    this.height=height;
    this.width=height;
    }

    setWidth(int width){
    this.height=width;
    this.width=width;
    }

    After all, the whole point of a square is that if you decide on the height you also have decided on the width, and vice versa.