Friday, April 11, 2014

When a Square is not a Rectangle!

Or how the Liskov Substitution Principle extends basic substitutability

The square vs. rectangle problem is a frequently used example of a class inheritance problem found throughout object-oriented programming. If you are not familiar with the problem, it addresses the issue of deriving a square shape from a rectangle. Mathematically a square is a rectangle. On the surface, modeling that relationship in OOP is simple, but when you drill down it gets a lot more complicated.


So applying the basics of object-oriented design the inheritance chain might look like this:


The principle of substitutability says that anywhere the system expects an object of a particular type, we are free to provide an object of that type or any type that extends that type. It works of course because the extension must provide at least the functionality of the type extended if nothing more. Here is a JUnit test that shows this in action; the test method only knows about a list of rectangles but the list contains squares:

public class TestRectangles {

@tab;private List<Rectangle> rectangles;

@tab;@Before
@tab;public void createRectangles() {

@tab;@tab;rectangles = new ArrayList<Rectangle>();
@tab;@tab;rectangles.add(new Rectangle());
@tab;@tab;rectangles.add(new Square());
@tab;}

@tab;@Test
@tab;public void TestRectangleSizes() {

@tab;@tab;for (Rectangle r : rectangles) {

@tab;@tab;@tab;r.setWidth(10);
@tab;@tab;@tab;r.setHeight(10);

@tab;@tab;@tab;assertEquals(40, r.getPerimeter());
@tab;@tab;@tab;assertEquals(100, r.getArea());
@tab;@tab;}
@tab;}
}


Well, that works for the rectangle and the square. I can set the width and the height, and I can get the perimeter and the area of the shape. So what's the problem?

On the surface, nothing. A square is a rectangle, so using a list of rectangles should work fine. And there are many examples of things besides squares and rectangles where this works just fine. But, a square must handle the setting of the width or height differently than a rectangle because they must always be the same. So setWidth in the square also sets the height, and setHeight sets the width.

Our mistake in the test was where we set the width and the height to the same value. If we set the height to another value, the assert succeeds for the rectangle but fails when we get to the square:

public class TestRectangles {

@tab;... @tab;public void TestRectangleSizes() {

@tab;@tab;for (Rectangle r : rectangles) {

@tab;@tab;@tab;r.setWidth(10);
@tab;@tab;@tab;r.setHeight(15);

@tab;@tab;@tab;assertEquals(50, r.getPerimeter());
@tab;@tab;@tab;assertEquals(150, r.getArea());
@tab;@tab;}
@tab;}
}


Why? Because when the height of the was set to 15 the width was set to match, so the area is actually 225, not 150. But the code was written for rectangles and doesn't expect that behavior!

Many people stop there when they learn OOP, substitutability defines all they want to know about polymorphism. But for successful OOP we really need to apply to the Liskov Substitution Principle (Liskov). We are interested in the facet of the LSP that goes beyond substitutability and defines that we cannot change the expected behavior of the object.

What happens when you change the width of a square? Or the height of a square? Well, they cannot be changed independently, right? But they can for a rectangle, no? So a square does not provide the expected behavior of a rectangle, and shouldn't be used in it's place! Our model violates the LSP.

Why is it important? The square vs. rectangle is a simple problem, but it exposes a basic design flaw. If there is client code that expects rectangles and we give it squares, it may fail because it does not expect both dimensions to change when one is set:

The only solution is for both square and rectangle to inherit from shape. They work similarly, but they are not really the same. The advantage is that we are protected from the client code seeing unexpected behavior from a square; the client code will be designed to either use squares and rectangles, or better yet just use the abstraction shape. The disadvantage is that we cannot leverage inheritance and may violate the DRY principle (do not repeat yourself) by placing similar code in both the rectangle and square:


So square vs. rectangle is a simple example to get the point across. I am sure that you can extend it to imagine more complicated scenarios where this design flaw could impact the reliability of an application. Consider what could happen when a class is extended to adapt to a future requirement, changes the expected behavior, and breaks client code that depends on it.

Related to this I am pretty sure that you have come across a final class in Java, or a sealed class in C#, both of which prevent the class from being extended with a subclass. Another way to look at blocking class extension is to that there is fundamentally no way to extend the class without changing the behavior, and that would violate the LSP!

On a final note I recently stumbled across a blog posted by one Alex Blewitt a few years back arguing against the LSP by arguing that Java and other languages do not and can never support it (Blewitt). I only mention this because I have seen several like this and I think that they fall into a trap that you need to watch out for: the idea that only the things we can enforce with compiler syntax are the viable targets.

But consider Bob Martin's first five fundamental principles in SOLID, and include Hunt and Thomas' DRY (Martin; Hunt). They are: the single responsibility principle, open for extension and closed for modification, the LSP, interface segregation, dependency inversion, and do not repeat yourself. If you are shaky on some of these then take a look at my post on SOLID and DRY. These principles are all used during design. You will not find a compiler that enforces any one of them, and yet they are all critical for designing applications which are reliable, adaptable, and maintainable. So the purpose of a language is not to enforce the principles; rather the syntax of a language should offer mechanisms to help support the principles.

So take your designs to the next level and make sure that you enforce the LSP alongside everything else!

References

See the references page.


No comments:

Post a Comment