héritage rubis vs mixins

127

Dans Ruby, puisque vous pouvez inclure plusieurs mixins mais étendre seulement une 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 en feriez-vous une classe? Ou en d'autres termes, pourquoi ne pas en faire toujours un module?

Je ne peux penser qu'à une seule raison pour laquelle vous voudriez une classe, et c'est si vous avez besoin d'instancier la classe. Dans le cas d'ActiveRecord :: Base, cependant, vous ne l'instanciez jamais directement. Cela n'aurait-il donc pas dû être un module à la place?

Brad Cupit
la source

Réponses:

176

Je viens de lire sur ce sujet dans The Well-Grounded Rubyist (excellent livre, au fait). L'auteur fait un meilleur travail d'explication que moi, alors je vais le citer:


Aucune règle ou formule unique ne donne toujours la bonne conception. Mais il est utile de garder à l'esprit quelques considérations lorsque vous prenez des décisions de classe contre 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 des classes, et que les caractéristiques ou propriétés des entités ou des choses sont mieux encapsulées dans des modules. De même, comme indiqué dans la section 4.1.1, les noms de classe 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 mélanger autant de modules qu'elle le souhaite. Si vous utilisez l'héritage, donnez la priorité à la création d'une relation superclasse / sous-classe sensible. N'utilisez pas la seule et unique relation de superclasse d'une classe pour doter la classe de ce qui pourrait s'avérer être l'un des nombreux ensembles de caractéristiques.

Pour résumer ces règles en 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 précise. Le camion descend de Vehicle (ce qui a du sens), 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 qui est transmise aux camions du fait que Truck est un descendant, ou forme spécialisée, de Véhicule.

Andy Gaskell
la source
1
L'exemple le montre parfaitement - le camion EST un véhicule - il n'y a pas de camion qui ne serait pas un véhicule.
PL J
1
L'exemple le montre parfaitement - TruckEST A Vehicle- il n'y a aucun Truckqui ne serait pas un Vehicle. Cependant, j'appellerais module peut-être SelfPropelable(:?) Hmm SelfPropeledsonne bien, mais c'est presque la même chose: D. Quoi qu'il en soit, je ne l'inclurais pas Vehiclemais dans Truck- car il y a des véhicules qui ne sont PAS SelfPropeled. Une bonne indication est également de demander - y a-t-il d'autres choses, PAS des véhicules qui SONT SelfPropeled? - Eh bien peut-être, mais je serais plus difficile à trouver. Donc Vehiclepourrait hériter de la classe SelfPropelling (en tant que classe, cela ne rentrerait pas comme SelfPropeled- car c'est plus un rôle)
PL J
39

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'espaces 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 c'est ce dernier module B, parce que vous l'avez inclus après module A. Maintenant, il est facile d'éviter ce problème: assurez - vous que tous module Aet module Bles constantes et méthodes » sont peu probables namespaces. 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 à de grandes équipes de programmeurs - vous ne devriez pas supposer que la personne implémentant class Cconnaît chaque nom dans la portée. Ruby vous permettra même de remplacer une constante ou une méthode d'un type différent . Je ne sais pas qui pourrait jamais être considéré comme un comportement correct.

Dan Barowy
la source
2
C'est une sage mise en garde. Réminiscence des multiples pièges de l'héritage en C ++.
Chris Tonkinson
1
Y a-t-il une bonne atténuation pour cela? Cela ressemble à une raison pour laquelle l'héritage multiple Python est une solution supérieure (ne pas essayer de démarrer une correspondance de langage p * ssing; juste comparer cette fonctionnalité spécifique).
Marcin
1
@bazz C'est génial et tout, mais la composition dans la plupart des langues est lourde. C'est aussi principalement pertinent dans les langues typées canard. Cela ne garantit pas non plus que vous n'obtiendrez pas d'états étranges.
Marcin
Ancien message, je sais, mais se révèle toujours dans les recherches. La réponse est en partie incorrecte - les C#sayhisorties B::HELLOnon pas parce que Ruby mélange les constantes, mais parce que ruby ​​résout les constantes de plus près à plus éloignées - donc HELLOréférencé dans Bserait toujours résolu à B::HELLO. Cela vaut même si la classe C a défini la sienne C::HELLOaussi.
Laas
13

Mon avis: les modules servent à partager le comportement, tandis que les classes servent à modéliser les relations entre les objets. Techniquement, vous pouvez simplement faire de tout une instance d'Object et mélanger les modules que vous souhaitez pour obtenir l'ensemble de comportements souhaité, mais ce serait une conception médiocre, aléatoire et plutôt illisible.

Mandrin
la source
2
Cela répond à la question de manière directe: l'héritage impose une structure organisationnelle spécifique qui peut rendre votre projet plus lisible.
emery
10

La réponse à votre question est largement contextuelle. Distillant l'observation de pubb, le choix est principalement motivé par le domaine considéré.

Et oui, ActiveRecord aurait dû être inclus plutôt qu'étendu par une sous-classe. Un autre ORM - datamapper - y parvient précisément!

nareshb
la source
4

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 du problème.

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 un comportement.

Tilo
la source
1

La meilleure façon dont je comprends les mixins sont les classes virtuelles. Les mixins sont des "classes virtuelles" qui ont été injectées dans la chaîne d'ancêtres d'une classe ou d'un module.

Lorsque nous utilisons "include" et lui passons un module, cela ajoute le module à la chaîne des ancêtres 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 a également une classe singleton. Les méthodes ajoutées à cette classe singleton peuvent être directement appelées sur l'objet et agissent donc comme des méthodes de «classe». Lorsque nous utilisons "extend" sur un objet et passons à 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 hooks pour les modules lorsqu'ils sont mélangés dans des classes / modules. includedest 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 extendedcrochet associé pour l'extension. 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 "class", et agit directement sur la classe cible (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
Donato
la source
-1

En ce moment, je pense au templatemodèle de conception. Cela ne serait tout simplement pas correct avec un module.

Géo
la source