In Progress
Unit 1, Lesson 1
In Progress

# Type and Class

Video transcript & code

Today we return to the `Quantity` class we've been slowly building over the past several weeks. Just to refresh your memory, we have defined two concrete quantity classes: `Feet` and `Meters`. With them we are able to represent physical quantities without the dangers of improperly mixing units.

``````require "./quantities"

Feet(23)                        # => #<Feet:23.0>
Meters(42)                      # => #<Meters:42.0>

Feet(23) + Meters(42)           # => #<Feet:160.79528>
``````

When we look at the definitions of `Feet` and `Meters`, we see something curious. We have been so successful in pulling common functionality up into the base `Quantity` class that the child classes are empty definitions.

``````class Feet < Quantity
end

class Meters < Quantity
end
``````

Ordinarily, the purpose of having child classes is to have a place to put behavior which is unique to a subset of instances. But clearly, `Feet` and `Meters` have no unique behavior of their own. All of their behavior is inherited from `Quantity`.

This begs a question: should `Feet` and `Meters` be child classes at all? What if we just had a `Quantity` class, which could be paramaterized with both an amount and a unit of measure, something like this?

``````Quantity.new(42, :feet)
``````

Let's go through the current `Quantity` class from beginning to end. Let's see what it would take to transition to using different instances to represent different units, instead of different classes.

We'll add a new attribute, `:unit`. We expect this attribute to be a symbol representing the unit of measure.

``````attr_reader :magnitude, :unit
``````

The initializer gets a new `unit` argument, with which it initializes the attribute.

``````def initialize(magnitude, unit)
@magnitude = magnitude.to_f
@unit      = unit
freeze
end
``````

Instead of using the class name when converting to a string representation, we use the unit.

``````def to_s
"#{@magnitude} #{unit}"
end
``````

We give the same treatment to the `#inspect` method.

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

All of the arithmetic operator methods need to be updated to pass the current unit to the newly created result object. In addition, our multiplication operator can no longer use case statement to switch on the type of its argument, because now we need to check if the argument is a `Quantity` and of the right unit. We switch to an `if` statement instead.

``````def +(other)
other = ensure_compatible(other)
self.class.new(@magnitude + other.to_f, unit)
end

def -(other)
other = ensure_compatible(other)
self.class.new(@magnitude - other.to_f, unit)
end

def *(other)
multiplicand = if other.is_a?(Numeric)
other
elsif other.is_a?(Quantity) && other.unit == unit
other.to_f
else
fail TypeError, "Don't know how to multiply by #{other}"
end
self.class.new(@magnitude * multiplicand, unit)
end

def /(other)
other = ensure_compatible(other)
self.class.new(@magnitude / other.to_f, unit)
end
``````

Similarly, we change the guard clause at the top of the spaceship operator to check both class and unit.

``````def <=>(other)
return nil unless other.is_a?(self.class) && other.unit == unit
magnitude <=> other.to_f
end
``````

We need to factor the unit into a quantity's hash value.

``````def hash
[magnitude, unit, self.class].hash
end
``````

We add the check for a matching unit to `#ensure_compatible`. Instead of sending `convert_to` with the class of the current quantity, it will now send the name of the current unit instead.

``````def ensure_compatible(other)
if other.is_a?(self.class) && other.unit == unit
return other
elsif other.respond_to?(:convert_to)
other.convert_to(unit)
else
fail TypeError
end
end
``````

Speaking of `convert_to`, we need to make a few changes to this method as well. Everywhere it once dealt with types, it now needs to deal with unit attributes.

``````def convert_to(target_unit)
ratio = ConversionRatio.find(unit, target_unit) or
fail TypeError, "Can't convert #{unit} to #{target_unit}"
self.class.new(magnitude * ratio.number, unit)
end
``````

Now we get to our `Feet` and `Meters` conversion functions. We'll keep these, both for convenience and so that our existing code continues to work. But we have to rewrite each of them substantially. Once again, a case statement doesn't cut it. Since we are now checking the given value for both whether it is a `Quantity` and for what `unit` it has, we need to switch to an `if` statement.

``````def Feet(value)
if value.is_a?(Quantity) && value.unit == :feet
value
else
value = Float(value)
Quantity.new(value, :feet)
end
end

def Meters(value)
if value.is_a?(Quantity) && value.unit == :meters
value
else
value = Float(value)
Quantity.new(value, :meters)
end
end
``````

Finally, we update our `ConversionRatio` registrations to register unit symbols instead of classes.

``````ConversionRatio.registry <<
ConversionRatio.new(:feet, :meters, 0.3048) <<
ConversionRatio.new(:meters, :feet, 3.28084)
``````

At last, we can remove the `Feet` and `Meters` classes.

Wow… this turned out to be a pretty big change! The question is, was it worth it?

I'll be honest. When I first set out to write this episode I thought it would be a good demonstration of how, in the absence of differing behavior, differentiated child classes aren't needed. We can differentiate just as easily between instances with different state.

But as I worked through the necessary changes, I realized something. Having objects be differentiated by class has subtle benefits which aren't always obvious until we try out the alternative. This single-class version of the code is longer. In many places where we previously just checked an object's class, we now have to check both class and unit. We can't leave out the class check, because that would invite a potential `NoMethodError` when we then check the `unit`. And of course we can't leave out the unit either. Everywhere we have to check compatibility of another `Quantity` object is now an opportunity to accidentally leave off part of this dual check.

The worst changes though, to my mind, are all the places we had to replace elegant case statements with longer `if/else` statements.

One way of looking at the type of an object is as just another attribute. One of our old `Feet` objects might have a `magnitude` of 23, and a `type` (or class) of `Feet`.

``````require "./quantities"

f = Feet(23)
f.to_f                          # => 23.0
f.class                         # => Feet
``````

From this point of view, we've just explicitly made the type into a regular attribute, called `:unit`.

But when we represent type as an ordinary attribute instead of as an object's class, we lose all the special conveniences that Ruby provides around classes. We can no longer use the classname as a shortcut for assigning type to a new object.

``````Feet.new(23)
``````

And we can no longer use a simple case statement to check that an object is both a `Quantity`, and a specific sub-type of `Quantity`, all at once.

``````case other
when Feet
# ...
end
``````

The concept of "type" is a tricky one. One of the refactorings in Martin Fowler's book on the subject is "Replace type code with class". In some cases, type codes in classes are hard to miss. It's not uncommon to run across attributes that are actually named "type". For instance, take this class for representing payments. Some payments might be recurring subscription payments, others might be one-time shopping cart payments. This is an obvious candidate for a parent-child class relationship.

``````Payment = Struct.new(:type, :amount)

Payment.new(:recurring, 9)
Payment.new(:shopping, 15)
``````

But other type codes are less obvious. For instance, the `unit` attribute in our `Quantity` class.

``````attr_reader :magnitude, :unit
``````

In the final analysis, I don't think it made sense to eliminate child classes for our `Quantity` class, even though their definitions were empty. Classes are useful for separating different behaviors, but that isn't their only value. So long as some kind of object may have different sub-types, and those types affect the logic of a program, we need a way of representing that differentiation. The Ruby way to encode type is with a class (or module), and we might as well take advantage of the benefits of that style of type encoding.