I'm going through Test First's Ruby projects and I've done most of them but 08_temperature_object has me pretty confused, not least because even though I have gotten it to work passing the tests I'm not sure if I did it how they wanted.
Here's what I came up with:
class Temperature
def initialize(options = {})
@options = options
#@options = Hash.new { |h, key| h[key] = [] }
end
#end
def in_fahrenheit
@options.key?(:f) ? @options[:f] : (@options[:c] * 9.0 / 5) + 32
end
def in_celsius
@options.key?(:c) ? @options[:c] : (@options[:f] - 32) * 5.0 / 9
end
def self.from_fahrenheit(num)
self.new(:f => num)
end
def self.from_celsius(num)
self.new(:c => num)
end
end
class Celsius < Temperature
def initialize(num, options = {})
@options = options
@options[:c] = num
end
def in_fahrenheit
super
end
def in_celsius
super
end
end
class Fahrenheit < Temperature
def initialize(num, options = {})
@options = options
@options[:f] = num
end
def in_fahrenheit
super
end
def in_celsius
super
end
end
From the instructions:
Remember to define the from_celsius
factory method as a class method, not an instance method.
???? Did I do it as a class method? Was there another/ better way than creating a new object?
The temperature object's constructor should accept an options hash which contains either a :celcius
entry or a :fahrenheit
entry.
???? I know I used a hash, but did I use an 'options hash'?
Factory Method is a design pattern... One way to implement this pattern in Ruby is via class methods
???? Is this written as a factory method?
Here's some samples of the Rspec tests for reference:
describe Temperature do
describe "can be constructed with an options hash" do
describe "in degrees fahrenheit" do
it "at 50 degrees" do
Temperature.new(:f => 50).in_fahrenheit.should == 50
end
describe "can be constructed via factory methods" do
it "in degrees celsius" do
Temperature.from_celsius(50).in_celsius.should == 50
Temperature.from_celsius(50).in_fahrenheit.should == 122
end
# test-driving bonus:
#
# 1. make two class methods -- ftoc and ctof
# 2. refactor to call those methods from the rest of the object
#
# run *all* the tests during your refactoring, to make sure you did it right
#
describe "utility class methods" do
end
# Here's another way to solve the problem!
describe "Temperature subclasses" do
describe "Celsius subclass" do
it "is constructed in degrees celsius" do
Celsius.new(50).in_celsius.should == 50
Celsius.new(50).in_fahrenheit.should == 122
end
it "is a Temperature subclass" do
Celsius.new(0).should be_a(Temperature)
end
end
Thanks, in advance
???? Did I do it as a class method?
Yes
Was there another/ better way than creating a new object?
Not really, this looks like the intended result of the lesson.
The temperature object's constructor should accept an options hash which contains either a :celcius entry or a :fahrenheit entry.
???? I know I used a hash, but did I use an 'options hash'?
Yes, sort of
Keeping the options hash around after construction, and referring it on all calculations is probably not the simplest solution. Internally, you could/should have a single numeric instance variable for temperature. It could even be Kelvin.
The use of sub-classes implies that they keep an internal variable in the appropriate units (although this is not required, the point of the tests is to drive public interface behaviour, to a large degree you are free to implement internals as you see fit)
There are cases for doing some of what you do in your post (e.g. properly round-tripping data, or lazy conversions), and the test scenario is very simple so the consequences of your choice are difficult to judge either way. So you may get other opinions.
The biggest problem with what you have done is this:
hash = { :c => 25 }
t = Temperature.new( hash )
# This reaches inside the new object and changes its state, breaking encapsulation:
hash[:c] = 30
Factory Method is a design pattern... One way to implement this pattern in Ruby is via class methods
???? Is this written as a factory method?
Yes
A factory method is just a method that returns a new and valid, properly initialized object.