Dans Ruby, puisque vous pouvez inclure plusieurs mixins mais étendre seulement une classe, il semble que les mixins soient préférés à l’inheritance.
Ma question: si vous écrivez du code qui doit être étendu / inclus pour être utile, pourquoi en feriez-vous un cours? Autrement dit, pourquoi ne pas toujours en faire un module?
Je ne peux que penser à une raison pour laquelle vous voudriez une classe, et c’est si vous devez instancier la classe. Dans le cas d’ActiveRecord :: Base, cependant, vous ne l’instanciez jamais directement. Alors, ne devrait-il pas y avoir un module à la place?
Je viens de lire à propos de ce sujet dans The Rubyist bien rodé (excellent livre, en passant). L’auteur fait un meilleur travail d’explication que je le ferais alors je le citerai:
Aucune règle ou formule unique n’aboutit toujours à la conception appropriée. Mais il est utile de garder quelques considérations à l’esprit lorsque vous prenez des décisions entre classes et modules:
Les modules n’ont pas d’instances. Il s’ensuit que les entités ou les choses sont généralement mieux modélisées dans des classes et que les caractéristiques ou les propriétés des entités ou des choses sont mieux encapsulées dans des modules. De manière correspondante, comme noté dans la section 4.1.1, les noms de classes ont tendance à être des noms, alors que les noms de modules sont souvent des adjectifs (Stack versus Stacklike).
Une classe ne peut avoir qu’une seule superclasse, mais elle peut combiner autant de modules qu’elle le souhaite. Si vous utilisez l’inheritance, donnez la priorité à la création d’une relation sensible entre les superclasses et les sous-classes. N’utilisez pas la seule et unique relation de classe d’une classe pour doter la classe de ce qui pourrait s’avérer être l’un de plusieurs ensembles de caractéristiques.
En résumant ces règles dans un exemple, voici ce que vous ne devriez pas faire:
module Vehicle ... class SelfPropelling ... class Truck < SelfPropelling include Vehicle ...
Vous devriez plutôt faire ceci:
module SelfPropelling ... class Vehicle include SelfPropelling ... class Truck < Vehicle ...
La deuxième version modélise les entités et les propriétés de manière beaucoup plus nette. Truck descend de Vehicle (ce qui est logique), alors que SelfPropelling est une caractéristique des véhicules (du moins, tous ceux qui nous intéressent dans ce modèle du monde) - une caractéristique transmise aux camions en raison du fait que Truck est un descendant, ou forme spécialisée du véhicule.
Je pense que les mixins sont une excellente idée, mais il y a un autre problème que personne n’a mentionné: les collisions d’espace de noms. Considérer:
module A HELLO = "hi" def sayhi puts HELLO end end module B HELLO = "you stink" def sayhi puts HELLO end end class C include A include B end c = C.new c.sayhi
Lequel gagne? En Ruby, il s’agit du module B
, car vous l’avez inclus après le module A
Maintenant, il est facile d’éviter ce problème: assurez-vous que toutes les constantes et méthodes du module A
et du module B
sont dans des espaces de nommage improbables. Le problème est que le compilateur ne vous avertit pas du tout lorsque des collisions se produisent.
Je soutiens que ce comportement ne s’adapte pas aux grandes équipes de programmeurs – vous ne devriez pas supposer que la personne qui implémente la class C
connaît chaque nom de la scope. Ruby vous permettra même de remplacer une constante ou une méthode d’un type différent . Je ne suis pas sûr que cela puisse jamais être considéré comme un comportement correct.
Ma prise: les modules sont destinés au comportement de partage, tandis que les classes servent à modéliser les relations entre les objects. Techniquement, vous pourriez tout faire une instance d’Object et mélanger les modules que vous voulez pour obtenir l’ensemble de comportements souhaité, mais ce serait une conception médiocre, aléatoire et peu lisible.
La réponse à votre question est en grande partie contextuelle. Distinguant l’observation de pubb, le choix dépend principalement du domaine considéré.
Et oui, ActiveRecord aurait dû être inclus plutôt que étendu par une sous-classe. Un autre ORM – datamapper – réalise précisément cela!
J’aime beaucoup la réponse d’Andy Gaskell – je voulais juste append que oui, ActiveRecord ne devrait pas utiliser l’inheritance, mais plutôt inclure un module pour append le comportement (principalement la persistance) à un modèle / une classe. ActiveRecord utilise simplement le mauvais paradigme.
Pour la même raison, j’aime beaucoup MongoId sur MongoMapper, car cela laisse au développeur la possibilité d’utiliser l’inheritance pour modéliser quelque chose de significatif dans le domaine du problème.
Il est sortingste que quasiment personne dans la communauté Rails n’utilise “Ruby Héritage” comme il est censé être utilisé – pour définir des hiérarchies de classes, pas seulement pour append un comportement.
La meilleure façon de comprendre les mixins est en tant que classes virtuelles. Les mixins sont des “classes virtuelles” qui ont été injectées dans la chaîne ancêtre d’une classe ou d’un module.
Lorsque nous utilisons “include” et que nous lui passons un module, il ajoute le module à la chaîne ancêtre juste avant la classe dont nous héritons:
class Parent end module M end class Child < Parent include M end Child.ancestors => [Child, M, Parent, Object ...
Chaque object de Ruby a également une classe singleton. Les méthodes ajoutées à cette classe singleton peuvent être directement appelées sur l’object et agissent donc comme des méthodes de “classe”. Lorsque nous utilisons “extend” sur un object et que nous lui passons un module, nous ajoutons les méthodes du module à la classe singleton de l’object:
module M def m puts 'm' end end class Test end Test.extend M Test.m
Nous pouvons accéder à la classe singleton avec la méthode singleton_class:
Test.singleton_class.ancestors => [#, M, #, ...
Ruby fournit des points d’ancrage pour les modules lorsqu’ils sont mélangés dans des classes / modules. included
est une méthode hook fournie par Ruby qui est appelée à chaque fois que vous incluez un module dans un module ou une classe. Tout comme inclus, il existe un crochet extended
associé pour l’extension. Il sera appelé lorsqu’un module est étendu par un autre module ou une autre classe.
module M def self.included(target) puts "included into #{target}" end def self.extended(target) puts "extended into #{target}" end end class MyClass include M end class MyClass2 extend M end
Cela crée un modèle intéressant que les développeurs peuvent utiliser:
module M def self.included(target) target.send(:include, InstanceMethods) target.extend ClassMethods target.class_eval do a_class_method end end module InstanceMethods def an_instance_method end end module ClassMethods def a_class_method puts "a_class_method called" end end end class MyClass include M # a_class_method called end
Comme vous pouvez le voir, ce module unique ajoute des méthodes d’instance, des méthodes de «classe» et agit directement sur la classe cible (en appelant a_class_method () dans ce cas).
ActiveSupport :: Concern encapsule ce modèle. Voici le même module réécrit pour utiliser ActiveSupport :: Concern:
module M extend ActiveSupport::Concern included do a_class_method end def an_instance_method end module ClassMethods def a_class_method puts "a_class_method called" end end end
En ce moment, je pense au template
conception de modèle. Ça ne serait pas correct avec un module.