eredità rubino vs mixins


127

In Ruby, poiché puoi includere più mixin ma estendere solo una classe, sembra che i mixin sarebbero preferiti rispetto all'eredità.

La mia domanda: se stai scrivendo un codice che deve essere esteso / incluso per essere utile, perché dovresti mai renderlo una classe? O in altri termini, perché non dovresti sempre renderlo un modulo?

Posso solo pensare a una delle ragioni per cui vorresti una classe, e cioè se devi istanziare la classe. Nel caso di ActiveRecord :: Base, tuttavia, non è mai possibile istanziarlo direttamente. Quindi non avrebbe dovuto essere un modulo?

Risposte:


176

Ho appena letto su questo argomento in The Well-Grounded Rubyist (ottimo libro, a proposito). L'autore fa un lavoro di spiegazione migliore di quanto vorrei, quindi lo citerò:


Nessuna singola regola o formula porta sempre al design giusto. Ma è utile tenere a mente un paio di considerazioni quando si prendono decisioni di classe contro modulo:

  • I moduli non hanno istanze. Ne consegue che le entità o le cose sono generalmente meglio modellate in classi e che le caratteristiche o le proprietà delle entità o delle cose sono meglio incapsulate in moduli. Di conseguenza, come notato nella sezione 4.1.1, i nomi delle classi tendono ad essere nomi, mentre i nomi dei moduli sono spesso aggettivi (Stack vs. Stacklike).

  • Una classe può avere solo una superclasse, ma può combinare tutti i moduli che desidera. Se stai usando l'ereditarietà, dai la priorità alla creazione di una relazione superclasse / sottoclasse sensata. Non utilizzare l'unica e unica relazione di superclasse di una classe per dotare la classe di quello che potrebbe rivelarsi solo uno dei numerosi insiemi di caratteristiche.

Riassumendo queste regole in un esempio, ecco cosa non dovresti fare:

module Vehicle 
... 
class SelfPropelling 
... 
class Truck < SelfPropelling 
  include Vehicle 
... 

Piuttosto, dovresti fare questo:

module SelfPropelling 
... 
class Vehicle 
  include SelfPropelling 
... 
class Truck < Vehicle 
... 

La seconda versione modella le entità e le proprietà in modo molto più ordinato. Il camion discende dal veicolo (il che ha senso), mentre l'auto-propulsione è una caratteristica dei veicoli (almeno, tutti quelli a cui teniamo in questo modello del mondo), una caratteristica che viene trasmessa ai camion in virtù del fatto che Truck è un discendente, o forma specializzata, del veicolo.


1
L'esempio lo mostra in maniera ordinata: il camion è un veicolo, non esiste un camion che non sia un veicolo.
PL J

1
L'esempio lo mostra in modo ordinato - TruckIS A Vehicle- non c'è Truckche non sarebbe un Vehicle. Comunque chiamerei module forse SelfPropelable(:?) Hmm SelfPropeledsuona bene, ma è quasi lo stesso: D. Ad ogni modo non lo includerei Vehiclema dentro Truck- poiché ci sono veicoli che NON SONO SelfPropeled. Anche una buona indicazione è chiedere: ci sono altre cose, NON veicoli che SONO SelfPropeled? - Beh, forse, ma sarei più difficile da trovare. Quindi Vehiclepotrebbe ereditare dalla classe SelfPropelling (come classe che non si adatterebbe come SelfPropeled- poiché questo è più di un ruolo)
PL J

39

Penso che i mixin siano un'ottima idea, ma qui c'è un altro problema che nessuno ha menzionato: le collisioni dello spazio dei nomi. Tener conto di:

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

Quale vince? In Ruby, risulta essere l'ultimo module B, perché l'hai incluso dopo module A. Ora, è facile evitare questo problema: assicurarsi che tutti module Ae module B's costanti e metodi sono in spazi dei nomi improbabili. Il problema è che il compilatore non ti avverte affatto quando si verificano collisioni.

Sostengo che questo comportamento non si adatta a grandi gruppi di programmatori: non dovresti presumere che la persona che implementa sia a class Cconoscenza di tutti i nomi nell'ambito. Ruby ti permetterà anche di ignorare una costante o un metodo di tipo diverso . Non sono sicuro che possa mai essere considerato un comportamento corretto.


2
Questa è una saggia parola di cautela. Ricorda le insidie ​​dell'ereditarietà multipla in C ++.
Chris Tonkinson,

1
C'è qualche buona mitigazione per questo? Questo sembra un motivo per cui l'ereditarietà multipla di Python sia una soluzione superiore (non tentare di avviare una corrispondenza di analisi del linguaggio; basta confrontare questa specifica funzionalità).
Marcin,

1
@bazz È fantastico e tutto, ma la composizione nella maggior parte delle lingue è ingombrante. È anche rilevante soprattutto nelle lingue dattiloscritte. Inoltre non garantisce che non si ottengano stati strani.
Marcin,

Vecchio post, lo so, ma risulta ancora nelle ricerche. La risposta è in parte errata: le C#sayhiuscite B::HELLOnon sono perché Ruby confonde le costanti, ma perché ruby ​​risolve le costanti da più vicino a lontano, quindi farebbe HELLOriferimento Ba B::HELLO. Questo vale anche se anche la classe C definita è propria C::HELLO.
Lasa il

13

La mia opinione: i moduli servono per condividere il comportamento, mentre le classi servono per modellare le relazioni tra oggetti. Tecnicamente potresti semplicemente trasformare tutto in un'istanza di Object e mescolarlo in qualsiasi modulo tu voglia ottenere l'insieme desiderato di comportamenti, ma sarebbe un design scadente, casuale e piuttosto illeggibile.


2
Questo risponde alla domanda in modo diretto: l'ereditarietà impone una struttura organizzativa specifica che può rendere il tuo progetto più leggibile.
smeriglio

10

La risposta alla tua domanda è ampiamente contestuale. Distillando l'osservazione di pubb, la scelta è principalmente guidata dal dominio in esame.

E sì, ActiveRecord avrebbe dovuto essere incluso piuttosto che esteso da una sottoclasse. Un altro ORM - datamapper - lo raggiunge esattamente!


4

Mi piace molto la risposta di Andy Gaskell - volevo solo aggiungere che sì, ActiveRecord non dovrebbe usare l'ereditarietà, ma piuttosto includere un modulo per aggiungere il comportamento (principalmente persistenza) a un modello / classe. ActiveRecord sta semplicemente usando il paradigma sbagliato.

Per lo stesso motivo, mi piace molto MongoId su MongoMapper, perché lascia allo sviluppatore la possibilità di utilizzare l'ereditarietà come un modo per modellare qualcosa di significativo nel dominio del problema.

È triste che praticamente nessuno nella comunità di Rails stia usando "l'ereditarietà di Ruby" nel modo in cui dovrebbe essere usato - per definire le gerarchie di classi, non solo per aggiungere comportamento.


1

Il modo migliore per capire i mixin è come classi virtuali. I mixin sono "classi virtuali" che sono state iniettate nella catena di antenati di una classe o di un modulo.

Quando usiamo "include" e gli passiamo un modulo, questo aggiunge il modulo alla catena degli antenati proprio prima della classe da cui stiamo ereditando:

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

Ogni oggetto in Ruby ha anche una classe singleton. I metodi aggiunti a questa classe singleton possono essere richiamati direttamente sull'oggetto e quindi fungono da metodi "class". Quando usiamo "estende" su un oggetto e passiamo l'oggetto a un modulo, stiamo aggiungendo i metodi del modulo alla classe singleton dell'oggetto:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

Possiamo accedere alla classe singleton con il metodo singleton_class:

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

Ruby fornisce alcuni hook per i moduli quando vengono miscelati in classi / moduli. includedè un metodo hook fornito da Ruby che viene chiamato ogni volta che includi un modulo in qualche modulo o classe. Proprio come incluso, c'è un extendedhook associato per extender. Verrà chiamato quando un modulo viene esteso da un altro modulo o 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

Questo crea un modello interessante che gli sviluppatori potrebbero usare:

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

Come puoi vedere, questo singolo modulo sta aggiungendo metodi di istanza, metodi "class" e agendo direttamente sulla classe target (chiamando a_class_method () in questo caso).

ActiveSupport :: Preoccupazione incapsula questo modello. Ecco lo stesso modulo riscritto per usare ActiveSupport :: Preoccupazione:

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

In questo momento, sto pensando al templatemodello di progettazione. Semplicemente non starebbe bene con un modulo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.