In Progress
Unit 1, Lesson 1
In Progress Video transcript & code

Hey, so remember back in Episode #214, we created a registry of `ConversionRatio` objects? Each one has a unit that it converts from, a unit it converts to, and a number representing the ratio.

``````class Feet; end
class Meters; end

ConversionRatio = Struct.new(:from, :to, :number) do
def self.registry
@registry ||= []
end

def self.find(from, to)
registry.detect{|ratio| ratio.from == from && ratio.to == to}
end
end

ConversionRatio.registry <<
ConversionRatio.new(Feet, Meters, 0.3048) <<
ConversionRatio.new(Meters, Feet, 3.28084)
``````

The code that uses this registry looks like this:

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

First this method looks up the appropriate conversion ratio for the units it is trying to convert. Then it uses the ratio's `number` as a multiplier.

This works great up until we decide to start representing temperature measurements using `Quantity` subclasses. The conversion from Celsius to Fahrenheit, and vice-versa, isn't a simple multiplication. The math is slightly more complex than that. And our current design doesn't accommodate this new twist.

Now, in designing object we don't have a crystal ball. We can't predict every new complication that might get thrown our way. However, we do have some broad guidelines to help us avoid common design traps. In this case, we've ignored one of these guidelines, and now we're suffering the consequences.

The guideline in question is the "tell, don't ask" principle. Meaning that as much as possible, we should tell objects what to do, rather than asking them about themselves.

Our `#convert_to` method uses `ConversionRatio` objects as simple data holders, reaching in to get the `ratio` and then using it in hardcoded calculation. It is asking, not telling.

What would it look like to tell instead of asking? Let's rewrite this code. `ConversionRatio` becomes `UnitConversion`. It loses the `number` attribute, and gains a new method named `#call`. This method is expected to be implemented in derived classes; we document this intention using a `NotImplementedError`, which we first saw in Episode #166. By the way, if you're curious why we opt for the name "call", see Episode #35.

We derive a `RatioConversion` subclass from this base class. It adds a `number` to the mix, and overrides `#call` to perform the conversion by using the number as a multiplier.

``````class Feet; end
class Meters; end

UnitConversion = Struct.new(:from, :to) do
def self.registry
@registry ||= []
end

def self.find(from, to)
registry.detect{|ratio| ratio.from == from && ratio.to == to}
end

def call(from_value)
raise NotImplementedError
end
end

class RatioConversion < UnitConversion
def initialize(from, to, number)
super(from, to)
@number = number
end

def call(from_value)
from_value * number
end
end

UnitConversion.registry <<
RatioConversion.new(Feet, Meters, 0.3048) <<
RatioConversion.new(Meters, Feet, 3.28084)
``````

Then we rewrite the `#convert_to` method. Instead of looking up a ratio, it's now looking up a conversion. And instead of reaching into the resulting object, it simply tells that object to perform its conversion, passing in the current object's magnitude.

``````def convert_to(target_type)
conversion = UnitConversion.find(self.class, target_type) or
fail TypeError, "Can't convert #{self.class} to #{target_type}"
target_type.new(conversion.call(magnitude))
end
``````

Now that we've switched from asking to telling, we easily add code to deal with temperature conversions. We add a new `UnitConversion` subclass we call `BlockConversion`. This class adds a `block` to the initializer, which it stows away for later use. Then, in the `#call` method, it invokes the block to perform the conversion.

To add a Celsius to Fahrenheit conversion, we create a new `BlockConversion` from `Celsius`, to `Fahrenheit`, and we pass a block to it. Inside the block, we perform the appropriate calculation: multiply by 9, divide by 5, and then add 32.

``````require "./conversion"

class Celsius; end
class Fahrenheit; end

class BlockConversion < UnitConversion
def initialize(from, to, &block)
super(from, to)
@block = block
end

def call(from_value)
@block.call(from_value)
end
end

UnitConversion.registry << BlockConversion.new(Celsius, Fahrenheit) {
|from_value|
((from_value * 9) / 5) + 32
}

UnitConversion.find(Celsius, Fahrenheit).call(32)
# => 89
``````

Should we have used this tell-don't-ask design from the get-go? That's debatable. While it's undoubtedly more flexible, and that flexibility turns out to be essential for a general-purpose unit conversion library, the new code is also more complex than the original version. There are more lines of code, and more classes.

There's one thing about which I have little doubt: once we identified the need for more flexibility in our conversion types, rewriting to this new design was the right thing to do. Any solution which kept the original value-holder design while attempting to accommodate temperature unit conversions would have pushed more and more complexity into the `Quantity` class. By redesigning the code to respect tell-don't-ask, we've kept separate responsibilities of representing quantities on the one hand, and converting between units on the other. Note that we ensured that the conversion objects deal only in raw values, so they don't even need to know the interface for `Quantity` objects. All they need to know about is the calculation needed to perform a conversion from one unit to another.

And that's all for today. Happy hacking!