51 votes

Ruby Design Pattern: Comment créer une classe fabrique extensible?

Ok, supposons que j'ai Ruby programme pour lire de contrôle de version des fichiers journaux et de faire quelque chose avec les données. (Je ne suis pas, mais la situation est analogue, et je m'amuse avec ces analogies). Supposons maintenant je veux soutenir Bazar et Git. Supposons que le programme sera exécuté, avec une sorte d'argument indiquant la version de logiciel de contrôle est utilisé.

Compte tenu de cela, je veux faire une LogFileReaderFactory qui donné le nom d'un programme de contrôle de version sera de retour un journal approprié lecteur de fichiers (sous-classé d'un générique) pour lire le fichier journal et cracher un canoniques de la représentation interne. Alors, bien sûr, je peux faire BazaarLogFileReader et GitLogFileReader et du code en dur dans le programme, mais je veux qu'il soit mis en place de telle manière que l'ajout du support pour un nouveau programme de contrôle de version est aussi simple que de dépenser une nouvelle classe de fichier dans le répertoire avec le Bazar et Git lecteurs.

Alors, maintenant, vous pouvez appeler le "faire-quelque chose-avec-le-log --logiciel git" et "faire-quelque chose-avec-le-log --logiciel de bazar", car il y a des lecteurs des journaux pour ceux. Ce que je veux, c'est pour qu'il soit possible d'ajouter simplement une SVNLogFileReader classe et le fichier dans le même répertoire et automatiquement être en mesure d'appeler des "faire-quelque chose-avec-le-log --logiciel svn", sans AUCUN changement pour le reste du programme. (Les fichiers peuvent bien sûr être nommé avec un motif spécifique et globbed dans le besoin d'appel.)

Je sais que cela peut être fait en Ruby... je n'ai juste pas comment je dois le faire... ou si je dois le faire.

97voto

Brian Campbell Points 101107

Vous n'avez pas besoin d'un LogFileReaderFactory; il suffit d'apprendre à votre LogFileReader classe comment instancier ses sous-classes:

class LogFileReader
  def self.create type
    case type 
    when :git
      GitLogFileReader.new
    when :bzr
      BzrLogFileReader.new
    else
      raise "Bad log file type: #{type}"
    end
  end
end

class GitLogFileReader < LogFileReader
  def display
    puts "I'm a git log file reader!"
  end
end

class BzrLogFileReader < LogFileReader
  def display
    puts "A bzr log file reader..."
  end
end

Comme vous pouvez le voir, la super-classe peut agir comme son propre usine. Maintenant, que diriez-enregistrement automatique? Eh bien, pourquoi ne pas simplement garder un hachage de notre enregistrée sous-classes, et d'enregistrer chacun lorsque nous définissons:

class LogFileReader
  @@subclasses = { }
  def self.create type
    c = @@subclasses[type]
    if c
      c.new
    else
      raise "Bad log file type: #{type}"
    end
  end
  def self.register_reader name
    @@subclasses[name] = self
  end
end

class GitLogFileReader < LogFileReader
  def display
    puts "I'm a git log file reader!"
  end
  register_reader :git
end

class BzrLogFileReader < LogFileReader
  def display
    puts "A bzr log file reader..."
  end
  register_reader :bzr
end

LogFileReader.create(:git).display
LogFileReader.create(:bzr).display

class SvnLogFileReader < LogFileReader
  def display
    puts "Subersion reader, at your service."
  end
  register_reader :svn
end

LogFileReader.create(:svn).display

Et là vous l'avez. Seulement, les diviser en un certain nombre de fichiers, et exiger d'eux de manière appropriée.

Vous devriez lire Peter Norvig de Modèles de Conception Dynamique de Langues si vous êtes intéressés par ce genre de chose. Il montre comment de nombreux modèles de conception sont en fait de travail autour de restrictions ou des insuffisances dans votre langage de programmation; et avec suffisamment puissant et flexible de la langue, vous n'avez pas vraiment besoin d'un modèle de conception, vous venez de mettre en œuvre ce que vous voulez faire. Il utilise Dylan et Common Lisp pour des exemples, mais beaucoup de ses points pertinents pour Ruby.

Vous pouvez également jeter un oeil à Pourquoi Poignante Guide de Ruby, en particulier les chapitres 5 et 6, mais seulement si vous pouvez traiter avec les surréalistes de la rédaction technique.

edit: Riffs hors de Jörg répondre maintenant, je le fais, comme la réduction de la répétition, et donc de ne pas répéter le nom du système de contrôle de version à la fois la classe et de l'enregistrement. En ajoutant ce qui suit à mon second exemple va vous permettre d'écrire beaucoup plus simple que la définition de classe, tout en restant assez simple et facile à comprendre.

def log_file_reader name, superclass=LogFileReader, &block
  Class.new(superclass, &block).register_reader(name)
end

log_file_reader :git do
  def display
    puts "I'm a git log file reader!"
  end
end

log_file_reader :bzr do
  def display
    puts "A bzr log file reader..."
  end
end

Bien sûr, dans le code de production, vous souhaitez peut-être en fait le nom de ces classes, par la génération d'une définition de constante basée sur le nom passé en, pour mieux les messages d'erreur.

def log_file_reader name, superclass=LogFileReader, &block
  c = Class.new(superclass, &block)
  c.register_reader(name)
  Object.const_set("#{name.to_s.capitalize}LogFileReader", c)
end

19voto

Jörg W Mittag Points 153275

C'est vraiment juste aussi Brian Campbell solution. Si vous aimez cela, veuillez upvote sa réponse, trop: il a fait tout le travail.

#!/usr/bin/env ruby

class Object; def eigenclass; class << self; self end end end

module LogFileReader
  class LogFileReaderNotFoundError < NameError; end
  class << self
    def create type
      (self[type] ||= const_get("#{type.to_s.capitalize}LogFileReader")).new
    rescue NameError => e
      raise LogFileReaderNotFoundError, "Bad log file type: #{type}" if e.class == NameError && e.message =~ /[^: ]LogFileReader/
      raise
    end

    def []=(type, klass)
      @readers ||= {type => klass}
      def []=(type, klass)
        @readers[type] = klass
      end
      klass
    end

    def [](type)
      @readers ||= {}
      def [](type)
        @readers[type]
      end
      nil
    end

    def included klass
      self[klass.name[/[[:upper:]][[:lower:]]*/].downcase.to_sym] = klass if klass.is_a? Class
    end
  end
end

def LogFileReader type

Ici, nous créons une méthode globale (plus comme une procédure, en fait) appelés LogFileReader, qui est le même nom que notre module LogFileReader. C'est légal en Ruby. L'ambiguïté est résolu comme ceci: le module sera toujours préférable, sauf quand c'est évidemment un appel de méthode, c'est à dire vous soit le mettre entre parenthèses, à la fin (Foo()) ou de passer un argument (Foo :bar).

C'est un truc qui est utilisé dans quelques endroits dans la stdlib, et aussi dans le Camping et d'autres cadres. Parce que des choses comme include ou extend ne sont pas réellement des mots-clés, mais les méthodes ordinaires qui prennent ordinaire paramètres, vous n'avez pas à passer un effectif Module comme argument, vous pouvez également passer quelque chose qui évalue à un Module. En fait, cela fonctionne même pour l'héritage, il est parfaitement légal d'écrire class Foo < some_method_that_returns_a_class(:some, :params).

Avec cette astuce, vous pouvez la faire ressembler à vous héritez d'une classe générique, même si Ruby n'ont pas de génériques. Il est utilisé par exemple dans la délégation de la bibliothèque, où vous faites quelque chose comme class MyFoo < SimpleDelegator(Foo), et ce qui se passe, c'est que l' SimpleDelegator méthode crée dynamiquement et renvoie un anonyme sous-classe de la SimpleDelegator classe, les délégués de tous les appels de méthode à une instance de l' Foo classe.

Nous utilisons une astuce similaire ici: nous allons créer dynamiquement un Module, qui, lorsqu'il est mélangé dans une classe, va automatiquement enregistrer la classe avec l' LogFileReader de registre.

  LogFileReader.const_set type.to_s.capitalize, Module.new {

Il y a beaucoup de choses dans juste cette ligne. Nous allons commencer à partir de la droite: Module.new crée un nouveau anonyme module. Le bloc passé, elle devient le corps du module, c'est essentiellement le même que l'utilisation de l' module mot-clé.

Maintenant, sur const_set. C'est une méthode pour la définition d'une constante. Donc, c'est la même chose que de dire FOO = :bar, à l'exception que l'on peut passer dans le nom de la constante en tant que paramètre, au lieu d'avoir à connaître à l'avance. Puisque nous sommes à l'appel de la méthode sur l' LogFileReader module, la constante sera défini à l'intérieur de cet espace, OIE, il sera nommé LogFileReader::Something.

Alors, quel est le nom de la constante? Eh bien, c'est l' type argument passé à la méthode par capitalisation. Donc, lorsque je passe en :cvs, la constante sera LogFileParser::Cvs.

Et que faisons-nous définir la constante? À notre nouvellement créé anonyme module, qui n'est plus anonyme!

Tout cela est vraiment juste une longwinded façon de dire module LogFileReader::Cvs, sauf que nous ne connaissions pas l' "Cvs" partie à l'avance, et ne pouvaient donc pas avoir écrit de cette façon.

    eigenclass.send :define_method, :included do |klass|

C'est le corps de notre module. Ici, nous utilisons define_method pour définir dynamiquement une méthode appelée included. Et nous ne sommes pas réellement définir la méthode sur le module lui-même, mais sur le module de eigenclass (via une petite méthode d'assistance que nous avons défini ci-dessus), ce qui signifie que la méthode ne deviendra pas une méthode d'instance, mais plutôt "statique" de la méthode (en Java/.Termes NETS).

included est en fait un crochet spécial de la méthode, qui est appelée par le Rubis de l'exécution, chaque fois qu'un module est inclus dans une classe, et la classe est transmis comme argument. Donc, notre module nouvellement créé a maintenant un crochet méthode qui vous permettra d'informer chaque fois qu'il est inclus quelque part.

      LogFileReader[type] = klass

Et c'est ce que notre crochet méthode: il enregistre la classe qui est passée dans le crochet de la méthode dans l' LogFileReader de registre. Et la clé qu'il l'enregistre sous, est l' type argument de l' LogFileReader méthode de la façon ci-dessus, qui, grâce à la magie des fermetures, il est en fait accessible à l'intérieur de l' included méthode.

    end
    include LogFileReader

Et le dernier mais non le moindre, nous incluons l' LogFileReader module dans l'anonymat module. [Note: j'ai oublié cette ligne dans l'exemple original.]

  }
end

class GitLogFileReader
  def display
    puts "I'm a git log file reader!"
  end
end

class BzrFrobnicator
  include LogFileReader
  def display
    puts "A bzr log file reader..."
  end
end

LogFileReader.create(:git).display
LogFileReader.create(:bzr).display

class NameThatDoesntFitThePattern
  include LogFileReader(:darcs)
  def display
    puts "Darcs reader, lazily evaluating your pure functions."
  end
end

LogFileReader.create(:darcs).display

puts 'Here you can see, how the LogFileReader::Darcs module ended up in the inheritance chain:'
p LogFileReader.create(:darcs).class.ancestors

puts 'Here you can see, how all the lookups ended up getting cached in the registry:'
p LogFileReader.send :instance_variable_get, :@readers

puts 'And this is what happens, when you try instantiating a non-existent reader:'
LogFileReader.create(:gobbledigook)

Cette nouvelle version permet de trois différentes façons de définir LogFileReaders:

  1. Toutes les classes dont le nom correspond au modèle <Name>LogFileReader sera automatiquement trouvé et enregistré en tant que LogFileReader pour :name (voir: GitLogFileReader),
  2. Toutes les classes qui mélange dans l' LogFileReader module et dont le nom correspond au modèle <Name>Whatever sera enregistrée pour l' :name gestionnaire (voir: BzrFrobnicator) et
  3. Toutes les classes qui mélange dans l' LogFileReader(:name) module, seront enregistrées pour l' :name gestionnaire, quel que soit leur nom (voir: NameThatDoesntFitThePattern).

Veuillez noter que c'est juste un très artificiel de démonstration. Il est, par exemple, certainement pas thread-safe. Il peut également présenter une fuite de mémoire. À utiliser avec prudence!

12voto

dlangevin Points 86

Une autre suggestion mineure pour la réponse de Brian Cambell -

En vous pouvez réellement inscrire automatiquement les sous-classes avec un rappel hérité. C'est à dire

 class LogFileReader

  cattr_accessor :subclasses; self.subclasses = {}

  def self.inherited(klass)
    # turns SvnLogFileReader in to :svn
    key = klass.to_s.gsub(Regexp.new(Regexp.new(self.to_s)),'').underscore.to_sym

    # self in this context is always LogFileReader
    self.subclasses[key] = klass
  end

  def self.create(type)
    return self.subclasses[type.to_sym].new if self.subclasses[type.to_sym]
    raise "No such type #{type}"
  end
end
 

Maintenant nous avons

 class SvnLogFileReader < LogFileReader
  def display
    # do stuff here
  end
end
 

Sans avoir besoin de l'enregistrer

7voto

Robert Wahler Points 2320

Cela devrait fonctionner aussi, sans qu'il soit nécessaire d'enregistrer des noms de classe

 class LogFileReader
  def self.create(name)
    classified_name = name.to_s.split('_').collect!{ |w| w.capitalize }.join
    Object.const_get(classified_name).new
  end
end

class GitLogFileReader < LogFileReader
  def display
    puts "I'm a git log file reader!"
  end
end
 

et maintenant

 LogFileReader.create(:git_log_file_reader).display
 

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X