Squaretangle

Here’s some Java code:

class Square {
    private int width;
    
    public Square(int width) {
        this.width = width;
    }
    
    public int getWidth() {
        return width;
    }
    
    public void setWidth(int width) {
        this.width = width;
    }
}

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

Here’s the same thing in Ruby:

class Square
  def initialize(width)
    @width = width
  end

  attr_accessor :width
end

class Rectangle
  def initialize(width, height)
    @width = width
    @height = height
  end

  attr_accessor :width
  attr_accessor :height
end

The Ruby version is so much more concise! Let’s work with that today.

Here’s the question of the day:

Should Square extend Rectangle, or should Rectangle extend Square?

A Rectangle is just a Square that also has a height. Inheritance lets us reuse the width property of Square. Let’s go for code reuse! Fantastic!…right??

class Square
  def initialize(width)
    @width = width
  end

  attr_accessor :width
end

class Rectangle < Square
  def initialize(width, height)
    super(width)
    @height = height
  end

  attr_accessor :height
end

def sq_area(square)
  square.width * square.width
end

def rect_area(rect)
  rect.width * rect.width
end

p sq_area(Square.new(2))          # good!
p rect_area(Rectangle.new(3, 7))  # good!

p sq_area(Rectangle.new(3, 7))    # oh no

The sq_area method’s one paramter has the implicit type of “object that has a width”. Both Square and Rectangle satisfy that type — so we can feed a Rectangle to sq_area and get the wrong answer. Terrible! Maybe we just need to be careful…but what if we want code that handles both squares and rectangles, maybe together in a collection, and computes all their areas correctly?

Answer 1, second attempt: Use conditionals

We could solve this by making a generic area method that checks the class of its argument:

def area(shape)
  if shape.is_a?(Rectangle)
    square.width * square.height
  else
    square.width * square.width
  end
end

We could also just check whether a height method exists, not worrying about specific class names:

def area(shape)
  if shape.respond_to?(:height)
    square.width * square.height
  else
    square.width * square.width
  end
end

Both these techniques are fairly common in Ruby library methods that need to handle a parameter with several possible types. In our situation, however, it feels like we’re on thin ice: we tried for code reuse and ended up with more code! Every single method that needs to work with both classes will have to jump through this same conditional logic. And if we ever added another sort of shape…not an ideal solution.

Answer 1, third attempt: Use method overrides

Here’s a different solution:

class Square
  def initialize(width)
    @width = width
  end

  attr_accessor :width

  def area
    self.width * self.width
  end
end

class Rectangle < Square
  def initialize(width, height)
    super(width)
    @height = height
  end

  attr_accessor :height

  def area
    self.width * self.height
  end
end

shapes = [
  Square.new(2),
  Rectangle.new(3, 7)
]

shapes.each do |shape|
  p shape.area  # Good!
end

But now any function that needs to work on both classes must move into the Square class. We can’t do this and expect it to work on Square too:

def scale(rect, factor)
    rect.width *= factor
    rect.height *= factor
end

…and we’ll hit a similar problem before if we do this instead:

def scale(square, factor)
    square.width *= factor
end

To take advantage of that subtype relationship, we have to shove scale into the superclass too:

class Square
  def initialize(width)
    @width = width
  end

  attr_accessor :width

  def area
    self.width * self.width
  end

  def scale(s)
    self.width *= s
  end

  # << More and more methods are going to appear here >>
end

class Rectangle < Square
  def initialize(width, height)
    super(width)
    @height = height
  end

  attr_accessor :height

  def area
    self.width * self.height
  end

  def scale(s)
    super(s)
    self.height *= s
  end
end

shapes = [
  Rectangle.new(3, 7),
  Square.new(2)
]

shapes.each do |shape|
  shape.scale(3)
  p shape.area  # OK
end

The fundamental problem is that inheritance does two things:

  • It creates an is-a relationship.
  • It reuses code.

These two things are in tension; in the code above, they are in direct opposition. We tried to reuse width, but now we have to jump through all kinds of hoops to keep the is-a relationship from breaking.

A common symptom of misuse of inheritance is a base class that becomes a dumping ground with a huge number of methods. Here’s a classic example from Microsoft’s old Windows API. (They really messed up with that one.)

Instead of aiming for code reuse, let’s honor the is-a relationship: every square is a rectangle, so Square should extend Rectangle:

class Rectangle
  def initialize(width, height)
    @width = width
    @height = height
  end
  
  attr_accessor :width
  attr_accessor :height
end

class Square < Rectangle
  def initialize(width)
    super(width, width)
  end

  # Ensure width and height are always the same
  
  def width=(width)
    @width = width
    @height = width
  end
  
  def height=(height)
    @width = height
    @height = height
  end
end

# Now this works!!
def area(rect)
  rect.width * rect.height
end

p area(Square.new(2))        # Yes!
p area(Rectangle.new(3, 7))  # Hooray!

This is a better approach. But…

def scale(rect, s)
  rect.width *= s
  rect.height *= s
end

r = Rectangle.new(3, 7)
scale(r, 2)
p area(r)    # OK, good

s = Square.new(2)
s.height = 3
scale(s, 2)  # oh no
p s          # OH NO
p area(s)    # OOOOOHHH NOOOOOOOO

What happened? Run the code. Trace through it. See if you can figure it out.

Liskov Substitution Principle (LSP):

Given two types S and T, if S ⊂ T, then all important properties / characteristics of T must also hold for S.

The result is that it should work to substitute a value of type S in any context that expects a T, because every S is a T.

Although Answer 2 honors the is-a relationship, both answers violate this principle.

Answer 1 violates LSP because the supertype (Square) has the property that all its sides are equal, but that does not hold true for the subtype (Rectangle).

Answer 2 violates LSP because the supertype (Rectangle) has the property that setting width does not alter height and vice versa, but that does not hold true for the subtype (Square).

It turns out there’s not really a good way to model the square/rectangle relationship using inheritance. This just isn’t a thing that OOP does well. The usual solution in practical contexts is to just have a single Rectangle class, and make code that needs a square check at runtime that the width and height are equal — leaving the type system out of it.