Wednesday 2 June 2010

The Liskov Substitution Principle (LSP)

The Liskov Substitution Principle was initially introduced by Barbara Liskov in a 1987 conference keynote address entitled Data abstraction and hierarchy. LSP is also part of SOLID, a group of five Object-Oriented Design Principles put together by Robert C. Martin in the early 2000s. 

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

The LSP's summary above looks quite obvious. If the calling code was written to use a base class, replacing it with a sub-class (inheritance) should make no difference to the calling code. That means, the calling code should not be changed and should be totally agnostic about which implementation is being used.

LSP's importance is noticed mainly when it is violated. If a subclass causes changes on the calling code, that means, the calling code needs to test which subclass it is dealing with (instanceof, casting, etc), the code is violating the Liskov Substitution Principle and also the Open Closed Principle. This violation causes high coupling, low cohesion and a cascade of changes.

Violation of Liskov Substitution Principle

public void drawShape(Shape s) {
    if (s instanceof Square) {
        drawSquare((Square) s);
    } else if (s instanceof Circle){
        drawCircle((Circle) s);
    }
}

The Liskov Substitution Principle also imposes a few rules that the sub-classes must obey. It bears a certain resemblance with Bertrand Meyer's Design by Contract in that it considers the interaction of subtyping with pre- and postconditions

...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.

This means the sub-classes must accept everything that the base class accepts (pre-condition) and must conform to all postconditions of the base class.

Example of a more subtle violation
public class Rectangle {
    private int height;
    private int width;

    public Rectangle(int height, int width) {
        this.height = height;
        this.width = width;
    }
    public int getHeight() {
        return this.height;
    }
    public void setHeight(int height) {
        this.height = height;
    }
    public int getWidth() {
        return this.width;
    }
    public void setWidth(int width) {
        this.width = width;
    }
}

public class Square extends Rectangle {

    public Square(int size) {
        super(size, size);
    }  
    public int getHeight() {
        return super.height;
    }
    public int getWidth() {
        return super.width;
    }
}

In the code above, a Square IS A Rectangle. Note that a rectangle can have different sizes for width and height, but in a square, height and width must be of the same size. What would happen in the following code is executed ?

Rectangle r = new Square();
r.setHeight(5);
r.setWidth(6);

We should not allow this to happen since that would make the Square object invalid. In a square, height and width must always be the same. A quick fix for this would be to override the setter methods:

public class Square extends Rectangle {

    public Square(int size) {
        super(size, size);
    }  
    public int getHeight() {
        return super.height;
    }
    public int getWidth() {
        return super.width;
    }
    public void setHeight(int height) {
        super.height = height;
        super.width = width;
    }
    public void setWidth(int width) {
        super.width = width;
        super.height = width;
    }
}

Although this approach would fix the problem, is a violation of the Liskov Substitution Principle since the methods will weaken (violate) the postconditions for the Rectangle setters, which state that dimensions can be modified independently.

One interesting thing to note is that if we analyse the Rectangle and Square classes in isolation, they are consistent and valid. However, when we look at how the classes can be used by a client program, we realise that the model is broken.

Every time you get yourself adapting a sub-class so that it does not break or its state does not become invalid if used in the context of a super-class, this is a clue that the sub-class should not be a sub-class at all.

In this example, maybe a Square is not a Rectangle. Square should not have height and width to start with. It should just have size. And since a Square is not a Rectangle, it should never be used in a Rectangle context.

public class Square {
    int size;
    
    public Square(int size) {
        this.size = size;
    }  
    public int getSize() {
        return this.size;
    }
    public void setSize(int size) {
        this.size = size;
    }
}

We can not validate a model in isolation. A model should be validated just in terms of its clients.

Source
http://en.wikipedia.org/wiki/Liskov_substitution_principle
http://www.objectmentor.com/resources/articles/lsp.pdf

0 comments:

Post a Comment