Factory methods in Ruby

2019-01-22 03:35发布

问题:

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:

If I make a factory method that is not called1 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

1. Calling the method factory works really well in this instructional example but IRL you may want to consider @AlexChaffee's advice in the comments.



回答2:

I did this today. Translated into vehicles it would look like this:

class Vehicle
  VEHICLES = {}

  def self.register_vehicle name
    VEHICLES[name] = self
  end

  def self.vehicle_from_name name
    VEHICLES[name].new
  end
end

class Bike < Vehicle
  register_vehicle 'mountain bike'
end

class Car < Vehicle
  register_vehicle 'ferrari'
end

I like that the labels for the classes are kept with the classes themselves, instead of having information about a subclass stored with the superclass. The constructor is not called new, but I see no benefit in using that particular name, and it would make things trickier.

> Vehicle.vehicle_from_name 'ferrari'
=> #<Car:0x7f5780840448>
> Vehicle.vehicle_from_name 'mountain bike'
=> #<Bike:0x7f5780839198>

Note that something needs to make sure those subclasses are loaded before vehicle_from_name is run (presumably these three classes would be in different source files), otherwise the superclass will have no way of knowing what subclasses exists, i.e. you cannot depend on autoload to pull those classes in when running the constructor.

I solved this by putting all subclasses in e.g. a vehicles subdirectory and adding this to the end of vehicle.rb:

require 'require_all'
require_rel 'vehicles'

Uses the require_all gem (found at https://rubygems.org/gems/require_all and https://github.com/jarmo/require_all)



回答3:

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


回答4:

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


回答5:

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 (Amazon link).



回答6:

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>


标签: ruby factory