6

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?

14
  • 2
    LSP relies on the idea of contracts. If a null object doesn't fit the contract (in this case setWidth(n); setHeight(m); assert(getWidth() == n);), then it does violate LSP.
    – Andrew
    Sep 12, 2017 at 18:09
  • Your use of getters and setters to reach through the object and access its internal data is the source of the problem. Check out this article. Sep 12, 2017 at 18:10
  • 2
    @M.Christian You could define it that way, and it wouldn't violate LSP, but it would violate the principle of least surprise. You'd have to make sure everyone else working on the code understands the contract, and that would be easiest to do by not calling it a Rectangle anymore.
    – Andrew
    Sep 12, 2017 at 18:59
  • 2
    @KaseySpeakman Subclassing square from rectangle results in LSP violation, as pointed out by the OP. A satisfying way to solve this “apparent paradox” while retaining the idea that a square is a special rectangle is to conclude that the classes we are provided do NOT model rectangles and squares. In geometry a rectangle is not something that changes, two distinct rectangles are just two distinct rectangles. Thus adding mutability to the rectangle turns it into something more than a rectangle, it is actually a function parametrising rectangles together with a distinguished point. 1/2 Sep 13, 2017 at 5:30
  • 1
    @MichaelGrünewald I agree with your analysis wrt Deformable. However, it seemed the goal was to explain/resolve the LSP violation. You might be able to subclass Square from Rectangle with immutable classes, but you would have to force Square to fit Rectangle's implementation details (length and height instead of sideLength), which (in non-toy examples) can make for brittle code due to tight coupling. Sep 13, 2017 at 14:35

5 Answers 5

5

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.

2

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.

1

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.

4
  • It's not intended as working code, it's a common bit of toy code used when discussing Liskov, as in blackwasp.co.uk/squarerectangle.aspx. Given that it's used as a common example of a Liskov violation, I don't think the lack of more functionality is the problem. And I know null objects aren't null pointers. I feel like you're trying to answer questions I'm not asking. Sep 13, 2017 at 0:04
  • @M.Christian There are many common examples which are wrong-headed thinking on OO. They led me astray for years, too. I am answering a different question from what you asked, because your question has a faulty premise. OO and Liskov are not about data hierarchies, which is what you create by inheritance of getters and setters. Sep 13, 2017 at 4:29
  • @M.Christian Also note in the example you linked, the author concludes that a correct implementation would have a Shape base class which does not include the dimensions as part of the contract. "The base class would not include properties for Height and Width, instead allowing the dimensions of different shapes to be handled by their implementations. A Rectangle subclass would have Height and Width properties and a Square may have a single Size value." Sep 13, 2017 at 4:38
  • The last time I saw a double XORed with 2 was in some air collision avoidance code from NASA, so you're in good company, sort of. Fortunately it never flew on our planes. Nov 6, 2017 at 13:19
1

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.

0

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.