Ruby中的工厂方法

19

如何使用最简洁、最符合Ruby风格的方式,让一个构造函数返回适当类型的对象?

更具体地说,举个例子:假设我有两个类BikeCar都继承自Vehicle。我想要实现以下目标:

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

我下面提出了一个解决方案,但它使用了allocate,似乎太过于依赖具体实现。还有其他方法吗?或者我的方法实际上可以吗?


你能使用混入吗?我的意思是,你是否必须拥有Bike和Car类?你能否有一个Bike和Car混入,可以在构造函数中包含或扩展到创建的对象中。 - Jim Deville
嗯,原则上我想是这样的 - 虽然这更像是一种hack - 正确的面向对象概念是生成的对象“是”自行车或汽车,而不是“像”自行车或汽车。 - Peter
你的代码如何知道需要哪种对象?是否涉及某种查找表? - Mike Woodhouse
2
看起来你有复杂的构造要求。将其放入继承层次结构中会使业务逻辑变得模糊。要么接受你需要特定的子类,如“MountainBike”等,要么像其他人建议的那样,在单独的工厂类中封装构造逻辑。这通常是最好的方法。 - Chris McCauley
6个回答

21

如果我创建一个工厂方法并且不将其命名为1 new 或者 initialize,我想那并不能真正回答问题"如何创建构造函数...",但我认为这就是我会做的...

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. 在这个示例中,调用方法factory效果非常好,但在实际情况下,您可能需要考虑@AlexChaffee在评论中提供的建议。


1
这是我会采取的方法,因为这样你就不必担心new的工作原理。 - Andrew Grimm
8
我建议不要称其为“工厂”。这会让模式和实现混淆。取而代之,可以将其命名为“create”或“from_style”等类似的名称。 - AlexChaffee

17

今天我做了这件事。如果用车辆来比喻的话,它会长成这个样子:

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

我喜欢类的标签与类本身保持在一起,而不是将有关子类的信息存储在超类中。构造函数没有被称为 new,但我认为使用那个特定的名称没有任何好处,而且会使事情变得更加棘手。

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

请注意,something需要确保在运行vehicle_from_name之前(假设这三个类位于不同的源文件中),这些子类已被加载,否则超类将无法知道存在哪些子类,即您不能依赖autoload在运行构造函数时拉取这些类。

我通过将所有子类放入例如vehicles子目录中并将其添加到vehicle.rb末尾来解决此问题:

require 'require_all'
require_rel 'vehicles'

使用require_all宝石(可在https://rubygems.org/gems/require_allhttps://github.com/jarmo/require_all找到)


根据我当时的理解,这段代码不应该能够正常工作。但是我当时没有意识到,只有在特定情况下,类变量引用才会与子类共享。 - clacke
不错的回答。但是,有没有人可以帮我理解为什么要使用类级别变量而不是常量?例如:VARIABLES = {} 而不是 @@vehicles = {} - Surya
一个常量会起作用,而且会更好,因为“某些情况”不那么令人困惑。:-)做得好。 - clacke
我真的很喜欢这个模式。谢谢! - Nils
2
@Surya,常量就是常量。一旦初始化,VEHICLES就不应该改变。 - Bibek Shrestha
3
在这种情况下,类变量是完全适合的。改变的常量完全违反了最小惊讶原则。 - Pierre

6

根据这里的内容改编,我有以下翻译:

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

我不知道你为什么要寻找比这更进一步的东西。虽然我不确定你所说的“实现重”,但这看起来像是 Ruby 的正确方式。 - KenB

4
什么样的包含模块可以替代超类呢?这样,您仍然可以使用 #kind_of? 方法,而且不会有默认的 new 方法挡路。
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

1
嗯,它很干净、流畅 - 我的担忧是它看起来更像是一个 hack 而不是一个“纯粹”的解决方案。一辆自行车 /是一种车辆/,而不是一辆自行车 /表现得像一种车辆/。这让我觉得正确的概念应该是子类而不是 mixin。 - Peter
2
这是一个公正的观点,尽管我认为这种区别在Java世界比Ruby世界更为明显。kind_of?is_a?对于模块都返回true,这意味着Ruby的思维方式是模块可以用于“is-a”隐喻。 - James A. Rosen

2
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

现在我们可以做...

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

你可以在书籍《Ruby设计模式》(亚马逊链接)中看到这个示例。

1
这与我想要的不同 - 我希望工厂方法在派生对象的超类中。 - Peter
5
如果(妻子允许?)优秀的。 - Dan

1

您可以通过将Vehicle#new更改为以下内容来使代码更简洁:

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

Vehicle.new 的最后一行使用三目运算符非常重要。如果没有检查 klass == self,我们会陷入无限循环并生成其他人之前指出的 StackError。请注意,我们必须用括号调用 super。否则,我们将使用 super 不希望接受的参数来调用它。

以下是结果:

> 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>

这个不起作用。你会得到一个 SystemStackError: stack level too deep。这说明了其中的一个主要问题:你不能同时覆盖 new 并调用它,除非进行一些额外的工作。 - Peter
只有当Bike和Car是Vehicle的子类时,系统堆栈错误才会发生。 - rampion
彼得,你说得对,我那个解决方案有点草率。让我修复它以避免StackError。 - Peter Wagenet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接