2009-10-04 14 views
18

¿Cuál es la forma más resbalosa y más parecida a Ruby de tener un único constructor que devuelva un objeto del tipo apropiado?Métodos de fábrica en Ruby

Para ser más específicos, aquí hay un ejemplo ficticio: decir que tengo dos clases Bike y Car qué subclase Vehicle. Quiero que esto:

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

he propuesto una solución a continuación, pero utiliza allocate que parece demasiado pesada-aplicación. ¿Cuáles son algunos otros enfoques, o el mío realmente está bien?

+0

Podría utilizar mixins? Lo que quiero decir es que tienes que tener clases para Bike y Car? ¿Podrías tener una mezcla de Bike y Car que se podría incluir o extender en el objeto creado en el constructor? –

+0

Hmm, supongo que en principio, aunque es más un hack, el concepto correcto de OO es que el objeto resultante es "una bicicleta o un automóvil, no se comporta como una bicicleta o un automóvil". – Peter

+0

¿Cómo sabe su código qué tipo de objeto se requiere? ¿Hay algún tipo de tabla de búsqueda involucrada? –

Respuesta

20

Si hago un método de fábrica que no se llama new o initialize, supongo que en realidad no responde a la pregunta "¿cómo puedo hacer un constructor de ... ...", pero Creo que esa es la forma en que lo haría ...

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. Llamar al método fábrica funciona muy bien en este ejemplo de instrucción pero IRL puede considerar el consejo de @AlexChaffee en los comentarios.

+1

Ese es el enfoque que tomaría, ya que entonces no tiene que preocuparse por cómo funciona 'new'. –

+8

Recomiendo no llamarlo "fábrica". Eso es confundir el patrón con la implementación. En lugar de nombrarlo algo así como "crear" o "de_estilo". – AlexChaffee

6

Adaptado de here, tengo

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

No sé por qué fuiste buscando más allá de esto. No estoy seguro exactamente a qué te refieres con "implementación pesada", pero esto se parece a The Ruby Way para mí. – KenB

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 

y ahora podemos hacer ...

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

Se puede ver este ejemplo en el libro patrones de diseño en Ruby (Amazon link).

+1

Esto es diferente de lo que estoy buscando: quiero que el método de fábrica esté en la superclase de los objetos derivados. – Peter

+5

'if (wife_allows?)' Brilliant –

1

Usted puede limpiar un poco las cosas cambiando a 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 

La última línea de Vehicle.new con la declaración ternaria es importante. Sin el cheque para klass == self nos quedamos atrapados en un bucle infinito y generamos el StackError que otros señalaban anteriormente. Tenga en cuenta que tenemos que llamar al super con paréntesis. De lo contrario, terminaríamos llamándolo con argumentos que super no espera.

Y aquí están los resultados:

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

Esto no funciona. Obtendrás un 'SystemStackError: stack level too deep'. Esto ilustra uno de los principales problemas: no puede anular nuevo y llamarlo sin un trabajo adicional – Peter

+0

El error de pila del sistema solo ocurre cuando Bike y Car son subclases del vehículo – rampion

+0

Peter, tiene razón, estaba un poco apresurado con esa solución. Déjame solucionarlo para evitar el StackError. –

4

¿Qué pasa con un módulo incluido en lugar de una superclase? De esta manera, todavía obtienes #kind_of? para trabajar, y no hay ningún valor predeterminado new que se interponga en el camino.

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

hmmm, está limpio y resbaladizo, mi preocupación es que parece más un truco que una solución "pura". una bicicleta/es un vehículo /, no una bicicleta/actúa como un vehículo /. Esto me sugiere que el concepto correcto es una subclase en lugar de mixin. – Peter

+2

Es un punto justo, aunque creo que esa distinción se siente más en el mundo de Java-ey que en el mundo de Ruby. 'kind_of?' y 'is_a?' ambos devuelven 'true' para los módulos, lo que implica para mí que la mentalidad de Ruby es que los módulos * pueden * usarse para metáforas" is-a ". –

17

Lo hice hoy.Traducido a los vehículos que se vería así:

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 

me gusta que las etiquetas de las clases se mantienen con las mismas clases, en lugar de tener información sobre una subclase almacenado con la superclase. El constructor no se llama new, pero no veo ningún beneficio en el uso de ese nombre en particular, y haría las cosas más complicadas.

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

Tenga en cuenta que algo necesita para asegurarse de que estas subclases se cargan antes de ejecutar vehicle_from_name (presumiblemente estas tres clases estarían en diferentes archivos de origen), de lo contrario la superclase no tendrá forma de saber qué subclases existe, es decir, no puede depender de la autocarga para extraer esas clases cuando ejecuta el constructor.

Lo resolví colocando todas las subclases en, p. Ej. un subdirectorio vehicles y añadiendo esto al final de vehicle.rb:

require 'require_all' 
require_rel 'vehicles' 

Utiliza la require_all joya (que se encuentra en https://rubygems.org/gems/require_all y https://github.com/jarmo/require_all)

+0

gran respuesta :-) – tbaums

+0

De acuerdo con mi comprensión cuando escribí esto, esto no debería haber funcionado. No me di cuenta en ese momento, pero esto solo funciona porque las referencias de variables de clase se comparten con subclases en ciertas circunstancias. – clacke

+0

Buena respuesta. Pero, ¿alguien puede ayudarme a entender por qué una variable de nivel de clase en lugar de una constante?como: 'VARIABLES = {}' en lugar de '@@ vehicles = {}' ?? – Surya

Cuestiones relacionadas