In Progress
Unit 1, Lesson 1
In Progress

# More Of Same

Video transcript & code

Yes, today we're once again working on representations of physical measurements. As I'm sure you recall, we have a class that represents quantities of feet. So far we've been mainly focused on preventing inadvertent mixing of `Feet` objects and measurements represented by raw numeric values. We are concerned about this, because we don't want to accidentally mix two different units of measure in one calculation.

But what about deliberate mixing of units? An application that is available internationally may well need to accept measurements in either meters or feet. So maybe it's time we added a class to represent meters as well.

Such a class is going to be identical to the `Feet` class in almost every detail. So we decide to just rename the `Feet` class to a more generic `Quantity`, and then inherit `Feet` and `Meters` classes from it. We also add a new `Meters` conversion function.

``````class Quantity
include Comparable

CoercedNumber = Struct.new(:value) do
def +(other) raise TypeError; end
def -(other) raise TypeError; end
def *(other)
other * value
end
def /(other) raise TypeError; end
end

alias_method :to_f, :magnitude
private :magnitude

def initialize(magnitude)
@magnitude = magnitude.to_f
freeze
end

def to_s
"#{@magnitude} feet"
end

def to_i
to_f.to_i
end

def inspect
"#<Feet:#{@magnitude}>"
end

def +(other)
raise TypeError unless other.is_a?(Feet)
Feet.new(@magnitude + other.to_f)
end

def -(other)
raise TypeError unless other.is_a?(Feet)
Feet.new(@magnitude - other.to_f)
end

def *(other)
multiplicand = case other
when Numeric then other
when Feet then other.to_f
else
raise TypeError, "Don't know how to multiply by #{other}"
end
Feet.new(@magnitude * multiplicand)
end

def /(other)
raise TypeError unless other.is_a?(Feet)
Feet.new(@magnitude / other.to_f)
end

def <=>(other)
raise TypeError unless other.is_a?(Feet)
other.is_a?(Feet) && magnitude <=> other.to_f
end

def hash
[magnitude, Feet].hash
end

def coerce(other)
unless other.is_a?(Numeric)
raise TypeError, "Can't coerce #{other}"
end
[CoercedNumber.new(other), self]
end

alias_method :eql?, :==
end

class Feet < Quantity
end

class Meters < Quantity
end

def Feet(value)
case value
when Feet then value
else
value = Float(value)
Feet.new(value)
end
end

def Meters(value)
case value
when Meters then value
else
value = Float(value)
Meters.new(value)
end
end
``````

Now let's try using some `Meters` objects.

We run into one issue immediately: `Meters` still identify themselves as `Feet`. We can also see this when converting `Meters` to a string.

``````require "./quantity"

Meters(23)                      # => #<Feet:23.0>
Meters(23).to_s                 # => "23.0 feet"
``````

We could fix this by specializing the inspect method in both `Meters` and `Feet` subclasses. But there's another way, that takes less code. We can just update those methods in the parent class to dynamically discover the name of their concrete child class. For the `#to_s` case, we use the lowercased version of the class name.

``````require "./quantity"

class Quantity
# ...
def inspect
"#<#{self.class}:#{@magnitude}>"
end

def to_s
"#{@magnitude} #{self.class.name.downcase}"
end
# ...
end
``````

Now meters show the correct type on inspection and conversion.

``````require "./quantity2"

Meters(23)                      # => #<Meters:23.0>
Meters(23).to_s                 # => "23.0 meters"
``````

Next, let's test out some math. We'll add two `Meters` objects together.

``````require "./quantity2"

Meters(23) + Meters(32)         # =>
# ~> /home/avdi/Dropbox/rubytapas/212-more-of-same/quantity.rb:36:in `+': TypeError (TypeError)
# ~>    from -:3:in `<main>'
``````

Whoops, we get a type error. Let's go check the code… ah, yes, we hardcoded the type check for `Feet`. Let's change that to also use the dynamically-discovered class.

``````require "./quantity2"

class Quantity
# ...
def +(other)
raise TypeError unless other.is_a?(self.class)
Feet.new(@magnitude + other.to_f)
end
# ...
end
``````

Let's try the addition again… well, we get no exception this time, but the result is a `Feet` object, instead of meters!

``````require "./quantity3"

Meters(23) + Meters(32)         # => #<Feet:55.0>
``````

So back we go to the method definition, where we can see that we also hardcoded the class to use for creating the result object. We update this to use the dynamic class instead.

By the way, you may have noticed that we always write `self.class` instead of just `class`. This is because `class` is a keyword in Ruby, and to use it alone would make the parser think we were trying to declare a new class. We have to qualify the `class` method with an explicit `self` receiver in order to make it unambiguous for the parser.

``````require "./quantity3"

class Quantity
# ...
def +(other)
raise TypeError unless other.is_a?(self.class)
self.class.new(@magnitude + other.to_f)
end
# ...
end
``````

Now when we add `Meters` together we get more `Meters`.

``````require "./quantity4"

Meters(23) + Meters(32)         # => #<Meters:55.0>
``````

The next obvious step would be to go through the whole `Quantity` class with search-and-replace, replacing every instance of `Feet` with `self.class`. This is a straightforward-enough step. But it still means our version control diff for this change is going to be a Christmas tree of little modifications.

Could we have avoided this? Yes, we could have. For the sake of example, up till now I've hardcoded the name `Feet` everywhere in this class. But ordinarily I wouldn't do this. Instead, my habit is to always prefer writing `self.class` rather than reiterating the name of a class inside itself. In this case, that would only have saved us some messy diffs. But the real value of this habit shines through when we start moving or copying individual methods from one class to another; for instance, when pushing a method up from a child class to a parent class. In those cases it can be all too easy to miss a hardcoded class reference.

And that's all I wanted to get across today: that in my opinion, it's best to make a habit of typing `self.class` instead of referencing a class' literal name inside itself. Happy hacking!