web-dev-qa-db-fra.com

ruby héritage vs mixins

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?

124
Brad Cupit

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.

175
Andy Gaskell

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 .

39
Dan Barowy

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.

12
Chuck

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!

10
nareshb

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.

4
Tilo

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
1
Donato