2009-10-04 11 views
18

Quelle est la méthode la plus lisse et la plus proche de Ruby pour qu'un seul constructeur retourne un objet du type approprié?Méthodes d'usine dans Ruby

Pour être plus précis, voici un exemple fictif: dire que j'ai deux classes Bike et Car qui sous-classe Vehicle. Je veux ceci:

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

J'ai proposé ci-dessous une solution, mais il utilise allocate qui semble trop lourde mise en œuvre. Quelles sont les autres approches, ou est-ce que le mien est correct?

+0

Pouvez-vous utiliser mixins? Qu'est-ce que je veux dire, est-ce que vous devez avoir des classes pour Bike and Car? Pourriez-vous avoir un mixin Bike and Car qui pourrait être inclus ou étendu dans l'objet créé dans le constructeur. –

+0

Hmm, je suppose qu'en principe - bien que ce soit plus un hack - le concept OO correct est que l'objet résultant 'est un' vélo ou une voiture, pas 'se comporte comme' un vélo ou une voiture. – Peter

+0

Comment votre code sait quel type d'objet est requis? Est-ce qu'une sorte de table de consultation est impliquée? –

Répondre

20

Si je fais une méthode de fabrication qui n'est pas appelé new ou initialize, je suppose que cela ne répond pas vraiment à la question « comment puis-je faire un ... constructeur ... », mais Je pense que ce que je ferais ...

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. Appelant la méthode usine fonctionne très bien dans cet exemple d'instruction mais IRL vous voudrez peut-être envisager @AlexChaffee dans les commentaires.

+1

C'est l'approche que je prendrais, car alors vous n'avez pas à vous soucier du fonctionnement de 'new'. –

+8

Je recommande de ne pas l'appeler "usine". C'est confondre le modèle avec la mise en œuvre. Au lieu de cela, nommez-le quelque chose comme "créer" ou "from_style". – AlexChaffee

6

Adapté de here, j'ai

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

Je ne sais pas pourquoi vous êtes allé chercher plus loin que cela. Je ne sais pas exactement ce que vous entendez par "lourdeur de mise en œuvre", mais cela ressemble à la Ruby Way pour moi. – 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 

et maintenant nous pouvons faire ...

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

Vous pouvez voir cet exemple dans le livre Design Patterns en Ruby (Amazon link).

+1

Ceci est différent de ce que je suis après - Je veux que la méthode d'usine soit dans la super-classe des objets dérivés. – Peter

+5

'if (wife_allows?)' Brillant –

1

Vous pouvez nettoyer un peu les choses en changeant 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 dernière ligne de Vehicle.new avec l'énoncé ternaire est important. Sans la vérification de klass == self, nous sommes bloqués dans une boucle infinie et génèrent l'erreur StackError que d'autres ont signalé plus tôt. Notez que nous devons appeler super avec des parenthèses. Sinon, nous finirions par l'appeler avec des arguments qui ne sont pas attendus par super.

Et voici les résultats:

> 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

Cela ne fonctionne pas. Vous obtiendrez un 'SystemStackError: niveau de la pile trop profond'. Cela illustre l'un des principaux problèmes: vous ne pouvez pas remplacer à la fois nouveau et appelez-le sans travail supplémentaire – Peter

+0

L'erreur de pile du système se produit uniquement lorsque Bike et Car sont des sous-classes de véhicule – rampion

+0

Peter, vous avez raison, j'étais un peu hâtive avec cette solution. Laissez-moi le réparer pour éviter le StackError. –

4

Qu'en est-il un module inclus au lieu d'une superclasse? De cette façon, vous obtenez toujours #kind_of? pour travailler, et il n'y a pas new par défaut qui entrave.

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, c'est propre, et lisse - mon souci est que cela ressemble plus à un hack qu'à une solution «pure». un vélo/est un véhicule /, pas un vélo/agit comme un véhicule /. Cela me suggère que le bon concept est une sous-classe plutôt qu'un mélange. – Peter

+2

C'est un bon point, même si je pense que cette distinction est plus ressentie dans le monde Java que dans le monde de Ruby. 'kind_of?' et 'is_a?' retournent tous deux 'true' pour les modules, ce qui implique pour moi que la mentalité Ruby est que les modules * peuvent * être utilisés pour les métaphores" is-a ". –

17

Je l'ai fait aujourd'hui.Traduit en véhicules, il ressemblerait à ceci:

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 

J'aime que les étiquettes pour les classes sont conservés avec les classes elles-mêmes, au lieu d'avoir des informations sur une sous-classe stockée avec la superclasse. Le constructeur n'est pas appelé new, mais je ne vois aucun avantage à utiliser ce nom particulier, et cela rendrait les choses plus délicates.

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

Notez que quelque chose doit faire en sorte que ces sous-classes sont chargées avant vehicle_from_name est exécuté (probablement ces trois classes seraient dans différents fichiers source), sinon la superclasse aura aucun moyen de savoir ce que les sous-classes existe, c'est-à-dire que vous ne pouvez pas compter sur autoload pour extraire ces classes lors de l'exécution du constructeur. J'ai résolu ce problème en mettant toutes les sous-classes, par exemple, dans le sous-groupe de la sous-classe. un sous-répertoire vehicles et en ajoutant à la fin de vehicle.rb:

require 'require_all' 
require_rel 'vehicles' 

Utilise la gemme require_all (trouvé à https://rubygems.org/gems/require_all et https://github.com/jarmo/require_all)

+0

bonne réponse :-) – tbaums

+0

Selon ma compréhension quand j'ai écrit ceci, cela n'aurait pas dû marcher. Je ne m'en suis pas rendu compte à l'époque, mais cela ne fonctionne que parce que les références de variable de classe sont partagées avec les sous-classes dans certaines circonstances. – clacke

+0

Bonne réponse. Mais, quelqu'un peut-il m'aider s'il vous plaît à comprendre pourquoi une variable de niveau de classe au lieu d'une constante ??comme: 'VARIABLES = {}' au lieu de '@@ vehicles = {}' ?? – Surya

Questions connexes