Search code examples
rubyrspectest-first

Test First's Temperature Object project - It passes all the tests, but is this what I was supposed to be learning?


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


Solution

  • ???? 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.