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
★★
(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があれば、テストを壊すこと無くリデザインがスムーズにできる