Our goal is to make it so that we have a stack equality operator that is both a stand-in replacement for bag's equality operator and does the right thing when dealing with a stack operation. Liskov suggests that you could define something like Bag#bag_equal?
, which Stack
must expose and implement to match Bag's behavior, and that you could add a second stack_equal?
method for doing ordered comparisons. However, this feels like a kludge to me, and so I tried to take another angle, following a pattern found in Ruby's Numeric
class (i.e. its integer?
and float?
methods):
require "set"
class Bag
def ==(other)
[Set.new(data), limit] == [Set.new(other.send(:data)), other.send(:limit)]
end
# returns true if the collection is an ordered collection,
# false otherwise. This defaults to returning false, but
# may be overridden by subtypes to return either true or false.
def ordered?
false
end
private
attr_accessor :data, :limit
end
class Stack
# other code similar to before
def ordered?
true
end
# use of send() is ugly here but I don't like making data public
def ==(other)
if other.ordered?
[other.send(:data), other.send(:limit)] == [data, limit]
else
[Set[*other.send(:data)], other.send(:limit)] == [Set[*data], limit]
end
end
private
attr_accessor :data, :limit
end
The end result is actually just as ugly (implementation-wise) as what Liskov recommended, but leads to slightly nicer behavior for the end user. Comparing two bags, or a bag and a stack uses bag equality behavior, comparing two stacks use stack equality behavior. But the fundamental difference between these two operations makes me wonder if it'd be better to avoid attempting to make these two objects have a subtype relationship at all, and have a simple Stack#to_bag
method that would make it easy to convert a stack into a Bag
. Since the whole exercise is a bit contrived, I won't take that line of reasoning further, but I'd be curious to hear the solutions that our readers came up with, so please do share if you have ideas!
The problems with the LSP violations between squares and rectangles can be trivially solved by making the structures immutable rather than mutable, which is probably a better design anyway. The way I might implement this is shown below, but it's easy to imagine how you can actually use composition rather than mixins for introducing the "Rectangular" concept.
module Rectangular
def area
width * height
end
end
class Rectangle
include Rectangular
def initialize(width, height)
self.width = width
self.height = height
end
attr_reader :width, :height
private
attr_writer :width, :height
end
class Square
include Rectangular
def initialize(size)
self.size = size
end
attr_reader :size
alias_method :width, :size
alias_method :height, :size
private
attr_writer :size
end