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