Friday, 2 January 2009

Is a Square a Rectangle?

I pre-ordered the Clojure book today, so I downloaded Clojure and had a peek at something that I know is interesting - "multimethods". I came across the following example statement:

(derive ::square ::rect)

i.e. a Square IS-A Rectangle in the Object-Oriented sense.

This rang a bell for me. Not a nice melodious one, but one that's a little off-key. A good teacher asked some colleagues and me the question "Is a Square a Rectangle?" a few years ago, and the answer wasn't all that clear ti us. But he showed us a realitively easy way to determine to some extent the validity of any IS-A relationship.

I will try and show you this principle by way of actually designing this hierarchy. First we design a simple Rectangle in Ruby:


class RectangleTest < Test::Unit::TestCase
def test_width_height

# Given
r = Rectangle.new(1,1)

# Then
assert_equal 1, r.width
assert_equal 1, r.height

# When
r.width = 5

# Then
assert_equal 5, r.width
assert_equal 1, r.height

# When
r.height = 2

# Then
assert_equal 5, r.width
assert_equal 2, r.height

end
end


If you think it's strange that I use a test as a design, I suggest you read this. I'm not even going to show the implementation since it's so simple. Next we will design our Square class:


class SquareTest < Test::Unit::TestCase
def test_width_height

# Given
s = Square.new(3)

# Then
assert_equal 3, s.width
assert_equal 3, s.height

# When
s.width = 5

# Then
assert_equal 5, s.width
assert_equal 5, s.height

# When
s.height = 2

# Then
assert_equal 2, s.width
assert_equal 2, s.height

end
end


And you can see that we make sure that when either the width or height has been set, we get the same value for both the width and height respectively. The same goes for the single-parameter constructor. Again, the implementation is trivial. 

Now, you might think that this design is OK, but it's not. We have to ensure that our inheritance doesn't break the Liskov Substritution Principle.

From Wikipedia:
"Liskov's notion of 'subtype' is based on the notion of substitutability; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness)."

This means that for our designs, the unit test for the Rectangle should pass when I substitute the Rectangle instance with a Square instance. Will it? No it won't. Because in the rectangle test, we ensure that when the width is set, the height is not affected, and vice versa. Using a Square instance here will break the test.

Another way of looking at it is to think of the contract of the setters, and the pre and post conditions of those contracts. According to the Liskov princple, (again, from Wikipedia):
  • Preconditions cannot be strengthened in a subclass.
  • Postconditions cannot be weakened in a subclass.
If we design the Square to force the width to be set when setting the height (and vice versa), we are weakening the postconditions of the Rectangle methods. I.e. the post condition that states that either the width or height should not be altered when setting the height or width respectively has been weakened.

In conclusion, here we have a Square that is NOT a Rectangle. The lesson to be learnt is that (unfortunately for us), IS-A in the real world or other domains does not necessarily imply IS-A in the OO world! 

P.S. If you remove the setters from the Rectangle, and only allow the fields to be set during construction, there is no violation of the Liskov Substitution Principle :)