quattro_4 scribble

scribble 落書き (調べた事をただ落書きする)

RubyTapas #211 - #217

211 Protected

In Ruby 2.1, the eqaulity method provided by Comparable hides exceptions and just returns false.
Note that this behavior will change in a future version of Ruby.

The important thing to understand is that protected access is for cases when objects of the same or related classes need access to each others' internals in order to fulfill their responsibilities.
This almost always means implementing operator methods, or some similar method that involves one object either comparing itself to, or combining itself with, another object of the same type

212 More Of Same

★★

dynamically-discovered class

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

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!

213 Conversion Protocol

★★

we split our Feet class into separate Feet and Meters classes, with a common base class called Quantity

ensure comparable

  def +(other)
    other = ensure_compatible(other)
  ...

  def ensure_compatible(other)
    fail TypeError unless other.is_a?(self.class)
    other
  end

to_meters

class Meters < Quantity
  # ...
  def ensure_compatible(other)
    if other.respond_to?(:to_meters)
      other.to_meters
    else
      super
    end
  end
end

I think of conventions like the #to_meters method as "conversion protocols".

The existence of the #to_meters method serves two purposes:
first, as a flag to let clients know that the object is convertable to Meters.
And second, as the means by which the conversion is accomplished.

214 Conversion Ratio

★★

book "Analysis Patterns", Martin Fowler

we define a new ConversionRatio class. ConversionRatios will be very simple value objects, so we use Struct to define it

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

class Quantity
  include Comparable
...

  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

The newly-added ConversionRatio class couldn't be much simpler: it's just a value holder with three slots

The key design insight here is the recognition that a conversion ration is not a property of a unit type; it's a property of the relationship between two unit types

215 Grep

Ruby has a grep as well. We can find it on any Enumerable collection

class Wildcard
  def ==(other)
    true
  end
end
ANY = Wildcard.new

ConversionRatio.registry.grep(ConversionRatio.new(Meters, Feet, ANY))

いまいち分からなかった

216 Tell, Don't Ask

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
  attr_reader :number
  def initialize(from, to, number)
    super(from, to)
    @number = number
  end

  def call(from_value)
    from_value * number
  end
end

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

217 Redesign

★★

f:id:quattro_4:20141005104536p:plain

f:id:quattro_4:20141005104529p:plain

f:id:quattro_4:20141005104522p:plain

f:id:quattro_4:20141005104514p:plain

f:id:quattro_4:20141005104504p:plain

(More) Acceptance Tests

Factors

  • Redesign, not a refactoring.
  • Acceptance tests
  • New class coexisting with old class
  • Interface flexibility
  • Piecemeal replacement of old code usage
  • Seams

Acceptance Testsがあれば、テストを壊すこと無くリデザインがスムーズにできる