Question

I keep thinking I have my head wrapped around the Liskov Substitution Principle, and then I realize I don't. Here's where I've gathered from StackExchange:

  1. Subclassing Square from Rectangle violates LSP because if you change the width of a Square the height changes too.
  2. Null Objects don't violate LSP.

Those seem to contradict each other to me. If this is bad:

square.setWidth(40);
square.setHeight(50);
width = square.getWidth(); // Returns 50 instead of 40. Liskov hates that.

Then why isn't this bad?

nullRectangle.setWidth(40);
nullRectangle.setHeight(50);
width = nullRectangle.getWidth(); // Returns 0 or null. Isn't that worse?

What am I missing here?

Was it helpful?

Solution

What you have found is simply edge case where Null object might not be good solution.

The point of null object is to make behavior same as if you checked for null beforehand.

In your case, if this code is expected behavior:

Rectangle nullRectangle = null;
int width = 0;
if (nullRectangle != null)
{
    nullRectangle.setWidth(40);
    nullRectangle.setHeight(50);
    width = nullRectangle.getWidth();
}
// width is 0 if nullRectangle is null

Ten null object as described in your code is logical substitute.

But the idea of having "null rectangle" doesn't make much sense from modeling perspective. Null object really is only usable as implementation of abstractions, where calling a method doesn't mean the caller is always expecting something. If caller of any method of the abstraction sees "no operation" or "no data returned" as valid result, then null object can be used. If instead a non-zero value is expected as return, then null object truly violates LSP.

OTHER TIPS

I think the Rectangle/Square problem is a result of a misguided domain analysis.

Mathematical rectangles / squares

From math, we all know that squares are special cases of rectangles, which we translate to Square being a subclass of Rectangle, and that's fine as long as we stay with the mathematical concept of rectangle and square which doesn't include changing width or height of the object. For immutable shapes, the inheritance model complies with the Liskov principle.

Mutable program objects

As soon as we introduce a setter, e.g. setHeight(), the mathematical analogy no longer holds, and we must do a new analysis.

What does setHeight(50) mean? When we see this in a Rectangle class, we all suppose that the width stays the same, so it modifies the shape's aspect ratio. And that's an operation that, when applied to a square, results in a non-square rectangle. So a Square.setHeight() method can't conform to the Rectangle-inspired contract as it should modify the object's class from Square to Rectangle (which is impossible in all programming languages I know).

So, having a mutable Square class inherit from Rectangle violates Liskov.

The source of evil here is mutability. If setHeight() were to return a fresh instance instead of changing the state of the original instance, we could solve the situation. Then calling setHeight() on a Square could easily return a Rectangle with the new dimensions. It could even inherit the implementation from Rectangle.

There's already an accepted answer, but I wanted to point out a couple of things.

Objects should have a behavior which they perform. Otherwise there is little reason to use one over a struct. Your example objects have no behavior whatsoever, only data which you have made more complicated to access via getters and setters. Your example violates Liskov mainly because it is not object-oriented anyway. It is procedurally reading and writing data with a specific structure.

I want to also mention that "Null Objects" do not mean null pointers. They mean an implementation of the class with a default behavior. See this video.

So what could LSP look like?

If I think of Shape2D in terms of what behavior I want it to do for me, one thing that comes to mind is area calculation. Because this will be different across shapes.

public class Shape2D
{
    public virtual double GetArea() { return 0.0; }
}

Here, I am using Shape2D as its own Null Object -- a shape that has no area. It can be extended by subclassing and overriding GetArea().

Returning double as the area is a questionable choice. Because there are other implications with area, like the unit of measure or that it can be infinite. Maybe an Area class should be created too if this were production code.

So let's create some implementations of 2-dimensional area-having things.

public class Square : Shape2D
{
    private double _sideLength;
    public Square(double sideLength) { _sideLength = sideLength; }
    public override double GetArea()
        { return _sideLength ^ 2 }
}

public class Rectangle : Shape2D
{
    private double _length;
    private double _height;
    public Rectangle(double length, double height) { ... }
    public override double GetArea()
        { return _length * _height; }
}

public class Circle : Shape2D
{
    private double _radius;
    public Circle(double radius) { _radius = radius; }
    public override double GetArea()
        { return Math.Pi * (_radius ^ 2); }
}

Now at this point, you might be wondering. "Why don't I just calculate the area in the constructor?" or "Why not just create a static method for different area calculations?" Well, what about complex 2D shapes? Like a concave octagon defined by vectors? Maybe the particular shape implementation is computationally expensive to calculate area? I think ultimately it requires some intuition to determine which way is correct. But in the example here, we are calculating it only when requested.

In the original question, you changed the width of the Rectangle by altering private data about the rectangle. But here, you would create a new Rectangle with different dimensions.

So this follows LSP, because for any method which takes a Shape2D, you could pass in a Square, Shape2D (Null Object), or any shape you might implement in the future like ConcaveOctagon without the method knowing it.

The "trick" of the classic Square-Rectangle LSP problem is that if we define a rectangle as an object that must satisfy the contract...

rectangle.setWidth(40);
rectangle.setHeight(50);
assert(rectangle.getWidth() == 40);

...then extend it to a square, then we violate that contract. This is because we are mixing up a custom definition of a rectangle and a classical definition. As such, we are trying to make the classical definition fit into a custom definition, which doesn't work. In essence, it is a parlour trick intended to make you think about sub-type behaviour and contractual obligations. We could just as easily show that the above assertion of a Rectangle object fails for even a rectangle with some cunning rewording:

rectangle.setTopEdgeLength(40);
rectangle.setBottomEdgeLength(50);
assert(rectangle.getTopEdgeLength() == 40);

Anyhow, enough of the riddles, let's have a look at the...

Classical Definition

Correctly put, squares and rectangles are both types of quadrilaterals, a quadrilateral being a polygon with 4 edges and 4 vertices. Classically, a square is a rectangle, but let's have a look at the Wikipedia definitions of a rectangle and a square:

  • Rectangle: A quadrilateral with four right angles
  • Square: A quadrilateral with four right angles and four equal sides

Notice something there? The only commonalities between a rectangle and a square are that both are quadrilaterals, and both have four right angles. No mention at all of rules around the length of the sides, as these are derived from the contract, not part of the contract. We can demonstrate this with some simple logical statements:

  • if a shape is a quadrilateral and the shape has four right angles, then opposite sides will be of equal length
  • if a shape is quadrilateral and the shape has four right angles and four equal sides, then the length of any side will be equal to the length any other side (this one is actually a tautology)

If we want to maintain the classical definition of rectangles and squares and represent them in code with the setWidth() and setHeight() methods in place, then the contract needs to be:

square.setWidth(40);
square.setHeight(50);
assert(square.isQuadrilateral());
assert(square.hasFourRightAngles());

Whether the width is still what we set it in line 1 is irrelevant, as that rule does not form part of the contractual obligations of the parent object Rectangle.


To sum up, the initial problem is one of contractual definitions. I'm not saying you shouldn't have a Rectangle object that satisfies rectangle.setWidth(40); rectangle.setHeight(50); assert(rectangle.getWidth() == 40);, but if you define your Rectangle object in that fashion, then when you extend it you need to ensure that the contract is still valid, otherwise you violate LSP.

I think there are two pitfalls in this example.

The first one is already pointed out in comments: inheritance based on internal structure is used, and it shouldn't be: it breaks object's encapsulation, it's fragile, it's procedural after all.

So the solution seems to use subtyping, like that:

interface IRectangle
{
    public function setWidth($width);

    public function setHeight($height);
}

class Rectangle implements IRectangle
{
    private $width;
    private $height;

    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function setHeight($height)
    {
        $this->height = $height;
    }
}

class Square implements IRectange
{
    private $side;

    public function setWidth($width)
    {
        $this->side = $width;
    }

    public function setHeight($height)
    {
        $this->side = $height;
    }
}

But there is the same contract violation.

So here is the second pitfall. In this context, characterized by objects' responsibilities, there should be two different abstractions, represented with two different interfaces: IRectangle and ISquare.

So it seems that this notorious example is just a logical puzzle, the one where there is some logical error but no one knows where it is. What are the rationals behind saying "square is a special case of a rectangle"? In what context is it a special case? What is the exact use case when a square acts like a rectangle? Probably there are some, but definitely it's not the one where rectangle's width is changed while height stays the same. So first of all this stupid example violates common sense, then OOP, and only after that it violates LSP.

Licensed under: CC-BY-SA with attribution
scroll top