tags:

views:

667

answers:

5

What is the slickest, most Ruby-like way to have a single constructor return an object of the appropriate type?

To be more specific, here's a dummy example: say I have two classes Bike and Car which subclass Vehicle. I want this:

Vehicle.new('mountain bike')  # returns Bike.new('mountain bike')
Vehicle.new('ferrari')        # returns Car.new('ferrari')

I've proposed a solution below, but it uses allocate which seems way too implementation-heavy. What are some other approaches, or is mine actually ok?

+1  A: 

Adapted from here, I have

class Vehicle
  def self.new(model_name)
    if model_name == 'mountain bike'  # etc.
      object = Bike.allocate
    else
      object = Car.allocate
    end
    object.send :initialize, model_name
    object
  end
end

class Bike < Vehicle
  def initialize(model_name)
  end
end

class Car < Vehicle
  def initialize(model_name)
  end
end
Peter
A: 
class VehicleFactory
  def new() 
    if (wife_allows?)
       return Motorcycle.new
    else
       return Bicycle.new
    end
  end
end

class vehicleUser 
  def doSomething(factory)
    a_vehicle = factory.new()
  end
end

and now we can do..

client.doSomething(Factory.new)
client.doSomething(Bicycle)    
client.doSomething(Motorcycle)

You can see this example in the book 'Design Patterns in Ruby'

http://www.amazon.com/Design-Patterns-Ruby-Russ-Olsen/dp/0321490452/ref=sr%5F1%5F1?ie=UTF8&amp;s=books&amp;qid=1254631723&amp;sr=8-1

rrichards
This is different to what I'm after - I want the factory method to be in the superclass of the derived objects.
Peter
+1  A: 

You can clean things up a bit by changing Vehicle#new to:

class Vehicle
  def self.new(model_name = nil)
    klass = case model_name
      when 'mountain bike' then Bike
      # and so on
      else                      Car
    end
    klass == self ? super() : klass.new(model_name)
  end
end

class Bike < Vehicle
  def self.new(model_name)
    puts "New Bike: #{model_name}"
    super
  end
end

class Car < Vehicle
  def self.new(model_name)
    puts "New Car: #{model_name || 'unknown'}"
    super
  end
end

The last line of Vehicle.new with the ternary statement is important. Without the check for klass == self we get stuck in an infinite loop and generate the StackError that others were pointing out earlier. Note that we have to call super with parentheses. Otherwise we'd end up calling it with arguments which super doesn't expect.

And here are the results:

> Vehicle.new
New Car: unknown # from puts
# => #<Car:0x0000010106a480>

> Vehicle.new('mountain bike')
New Bike: mountain bike # from puts
# => #<Bike:0x00000101064300>

> Vehicle.new('ferrari')
New Car: ferrari # from puts
# => #<Car:0x00000101060688>
Peter Wagenet
This doesn't work. You'll get a `SystemStackError: stack level too deep`. This illustrates one of the main problems: you can't both override new and call it without some extra work
Peter
The system stack error only happens when Bike and Car are subclasses of Vehicle
rampion
Peter, you're right, I was a bit hasty with that solution. Let me fix it to avoid the StackError.
Peter Wagenet
+2  A: 

What about an included Module instead of a superclass? That way, you still get #kind_of? to work, and there's no default new that gets in the way.

module Vehicle
  def self.new(name)
    when 'mountain bike'
      Bike.new(name)
    when 'Ferrari'
      Car.new(name)
    ...
    end
  end
end

class Bike
  include Vehicle
end

class Car
  include Vehicle
end
James A. Rosen
hmmm, it's clean, and slick - my concern is that it seems more like a hack than a 'pure' solution. a Bike /is a Vehicle/, not a Bike /acts like a Vehicle/. This suggests to me that the right concept is a subclass rather than a mixin.
Peter
It's a fair point, though I think that distinction is more felt in the Java-ey world than the Ruby world. `kind_of?` and `is_a?` both return `true` for modules, which implies to me that the Ruby mindset is that modules *can* be used for "is-a" metaphors.
James A. Rosen
+1  A: 

If I make a factory method that is not called new or initialize, I guess that doesn't really answer the question "how do I make a ... constructor ...", but I think that's how I would do it...

class Vehicle
  def Vehicle.factory vt
    { :Bike => Bike, :Car => Car }[vt].new
  end
end

class Bike < Vehicle
end

class Car < Vehicle
end


c = Vehicle.factory :Car
c.class.factory :Bike
DigitalRoss