Ruby modèle de conception: Comment faire un extensible usine de classe?
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.
Vous devez vous connecter pour publier un commentaire.
Vous n'avez pas besoin d'un LogFileReaderFactory; il suffit d'apprendre à votre LogFileReader classe comment instancier ses sous-classes:
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 enregistrer chaque moment de notre définir:
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 Les 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 ainsi.
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.
modifier: 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.
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.
@@
) ont surprenant comportement en Ruby et doit être évitée. Utiliser une variable d'instance de classe au lieu de cela, utilisez un@
dans une classe de contexte. Vous obtiendrez une variable qui est portée à la classe elle-même, mais se comporte en moins de moyens détournés. (Dans ce cas, on@
sur@subclasses
fonctionne parfaitement.)C'est vraiment juste aussi Brian Campbell solution. Si vous aimez ce, veuillez upvote son réponse, trop: il a fait tout le travail.
Ici, nous créons une méthode globale (plus comme une procédure, en fait) appelé
LogFileReader
, qui est le même nom que notre moduleLogFileReader
. 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
ouextend
ne sont pas réellement des mots-clés, mais les méthodes ordinaires qui prennent ordinaire paramètres, vous n'avez pas à transmettre une réelleModule
comme argument, vous pouvez également passer quelque chose qui évalue à unModule
. En fait, cela fonctionne même pour l'héritage, il est parfaitement légal d'écrireclass 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 leSimpleDelegator
méthode dynamiquement crée et retourne un anonyme sous-classe de laSimpleDelegator
classe, qui délègue tous les appels de méthode sur une instance de laFoo
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 leLogFileReader
registre.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 lamodule
mot-clé.Maintenant, à
const_set
. C'est une méthode pour la définition d'une constante. Donc, c'est la même chose que de direFOO = :bar
, sauf 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 leLogFileReader
module, la constante sera défini à l'intérieur de cet espace, OIE, il sera nomméLogFileReader::Something
.Donc, ce est le nom de la constante? Eh bien, c'est la
type
argument passé à la méthode par capitalisation. Donc, lorsque je passe en:cvs
, la constante seraLogFileParser::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.C'est le corps de notre module. Ici, nous utilisons
define_method
pour définir dynamiquement une méthode appeléeincluded
. 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.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 le
LogFileReader
de registre. Et la clé qu'il l'enregistre sous, est letype
argument de laLogFileReader
méthode de la façon ci-dessus, qui, grâce à la magie des fermetures, il est en fait accessible à l'intérieur de laincluded
méthode.Et le dernier mais non le moindre, nous incluons le
LogFileReader
module dans l'anonymat module. [Note: j'ai oublié cette ligne dans l'exemple original.]Cette nouvelle version permet de trois différentes façons de définir
LogFileReader
s:<Name>LogFileReader
sera automatiquement trouvé et enregistré comme unLogFileReader
pour:name
(voir:GitLogFileReader
),LogFileReader
module et dont le nom correspond au modèle<Name>Whatever
sera enregistrée pour le:name
gestionnaire (voir:BzrFrobnicator
) etLogFileReader(:name)
module, seront enregistrés pour la: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!
Une légère suggestion pour Brian Campbell réponse -
Vous pouvez réellement auto-enregistrer les sous-classes avec un hérité de rappel. I. e.
Maintenant, nous avons
Avec pas besoin de s'inscrire, il
Cela devrait fonctionner aussi, sans la nécessité pour l'enregistrement des noms de classe
et maintenant
LogFileReader.create(:string)
renvoie une nouvelle Chaîne vide! L'avantage de l'utilisation d'un registre de tous les types, c'est que le symbole :git utilisés pour trouver que la classe de lecteur peut être utilisé pour la recherche d'autres choses. Par exemple, un journal de la coiffe.name.to_s.split('_').collect!{ |w| w.capitalize }.join
peut être remplacé parname.camelcase