Dans Ruby, puisque vous pouvez inclure plusieurs mixins mais étendre une seule classe, il semble que les mixins soient préférés à l'héritage.
Ma question: si vous écrivez du code qui doit être étendu/inclus pour être utile, pourquoi voudriez-vous en faire une classe? Ou, autrement dit, pourquoi n'en feriez-vous pas toujours un module?
Je ne peux penser qu'à une raison pour laquelle vous voudriez un cours, et c'est si vous avez besoin d'instancier le cours. Dans le cas d'ActiveRecord :: Base, cependant, vous ne l'instanciez jamais directement. Cela n'aurait-il pas dû être un module à la place?
Je viens de lire ce sujet dans Le rubyiste bien fondé (grand livre, au fait). L'auteur explique mieux que moi, je vais donc le citer:
Aucune règle ou formule unique n'aboutit toujours à la bonne conception. Mais il est utile de garder à l'esprit quelques considérations lorsque vous prenez des décisions classe par module:
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 les classes, et les caractéristiques ou propriétés des entités ou des choses sont mieux encapsulées dans modules. De même, comme indiqué dans la section 4.1.1, les noms de classe ont tendance à être des noms, tandis que les noms de module sont souvent des adjectifs (Stack versus Stacklike).
Une classe ne peut avoir qu'une seule superclasse, mais elle peut mélanger autant de modules qu'elle le souhaite. Si vous utilisez l'héritage, donnez la priorité à la création d'un relation sensée superclasse/sous-classe. N'utilisez pas la seule et unique relation de superclasse d'une classe pour doter la classe de ce qui pourrait se révéler être l'un des nombreux ensembles de caractéristiques.
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. Le camion descend du véhicule (ce qui a du sens), tandis que l'autopropulsion est une caractéristique des véhicules (au moins, tous ceux qui nous intéressent dans ce modèle du monde) - une caractéristique qui est transmise aux camions en raison du fait que le camion est un descendant, ou forme spécialisée, de Véhicule.
Je pense que les mixins sont une excellente idée, mais il y a un autre problème ici 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? Dans Ruby, il s'avère que ce soit le dernier, module B
, Car vous l'avez inclus après module A
. Maintenant, il est facile d'éviter ce problème: assurez-vous que toutes les constantes et méthodes de module A
Et module B
Se trouvent dans des espaces de nom peu probables. 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 devez pas supposer que la personne implémentant class C
Connaît tous les noms de la portée. 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 pourrait jamais être considéré comme un comportement correct .
Mon point de vue: les modules sont destinés au partage des comportements, tandis que les classes sont destinées à modéliser les relations entre les objets. Techniquement, vous pourriez simplement faire de tout une instance d'Object et mélanger dans les modules que vous voulez pour obtenir l'ensemble de comportements souhaité, mais ce serait une conception médiocre, aléatoire et plutôt illisible.
La réponse à votre question est largement contextuelle. Distillant l'observation de pubb, le choix est principalement dicté par le domaine considéré.
Et oui, ActiveRecord aurait dû être inclus plutôt qu'étendu par une sous-classe. Un autre ORM - datamapper - atteint précisément cela!
J'aime beaucoup la réponse d'Andy Gaskell - je voulais juste ajouter que oui, ActiveRecord ne devrait pas utiliser l'héritage, mais plutôt inclure un module pour ajouter le comportement (principalement la persistance) à un modèle/classe. ActiveRecord utilise simplement le mauvais paradigme.
Pour la même raison, j'aime beaucoup MongoId par rapport à MongoMapper, car cela laisse au développeur la possibilité d'utiliser l'héritage comme moyen de modéliser quelque chose de significatif dans le domaine problématique.
Il est triste que pratiquement personne dans la communauté Rails n'utilise "l'héritage Ruby" comme il est censé être utilisé - pour définir les hiérarchies de classes, pas seulement pour ajouter du 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 lui transmettons 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 objet de Ruby possède également une classe singleton. Les méthodes ajoutées à cette classe singleton peuvent être directement appelées sur l'objet et elles agissent donc comme des méthodes de "classe". Lorsque nous utilisons "extend" sur un objet et passer l'objet un module, nous ajoutons les méthodes du module à la classe singleton de l'objet:
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
=> [#<Class:Test>, M, #<Class:Object>, ...
Ruby fournit des crochets pour les modules lorsqu'ils sont mélangés dans des classes/modules. included
est une méthode de 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 y a un extended
associé hook pour extend. Il sera appelé lorsqu'un module est étendu par un autre module ou 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 pourraient 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