Ereditare metodi di classe da moduli / mixin in Ruby


95

È noto che in Ruby, i metodi di classe vengono ereditati:

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

Tuttavia, mi sorprende che non funzioni con i mixin:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

So che il metodo #extend può farlo:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

Ma sto scrivendo un mixin (o, piuttosto, vorrei scrivere) contenente sia metodi di istanza che metodi di classe:

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

Ora quello che vorrei fare è questo:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

Voglio che A, B erediti sia i metodi di istanza che di classe dal Commonmodulo. Ma ovviamente non funziona. Quindi, non esiste un modo segreto per far funzionare questa eredità da un singolo modulo?

Mi sembra inelegante dividerlo in due diversi moduli, uno da includere e l'altro da estendere. Un'altra possibile soluzione sarebbe usare una classe Commoninvece di un modulo. Ma questa è solo una soluzione alternativa. (E se ci sono due serie di funzionalità comuni Common1ed Common2e abbiamo davvero bisogno di avere mixins?) C'è qualche motivo per cui profondo metodo di classe eredità non funziona da mixins?



1
Con la distinzione che qui so che è possibile - chiedo il modo meno brutto di farlo e per i motivi per cui la scelta ingenua non funziona.
Boris Stitnicky

1
Con più esperienza, ho capito che Ruby sarebbe andato troppo oltre nell'indovinare l'intento del programmatore se l'inclusione di un modulo avesse aggiunto anche i metodi del modulo alla classe singleton di includer. Questo perché i "metodi del modulo" in realtà non sono altro che metodi singleton. I moduli non sono speciali per avere metodi singleton, sono speciali per essere spazi dei nomi in cui sono definiti metodi e costanti. Lo spazio dei nomi è completamente estraneo ai metodi singleton di un modulo, quindi in realtà l'ereditarietà delle classi dei metodi singleton è più sorprendente della sua mancanza nei moduli.
Boris Stitnicky

Risposte:


171

Un idioma comune è usare includedhook e inject da lì metodi di classe.

module Foo
  def self.included base
    base.send :include, InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def bar1
      'bar1'
    end
  end

  module ClassMethods
    def bar2
      'bar2'
    end
  end
end

class Test
  include Foo
end

Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"

26
includeaggiunge metodi di istanza, extendaggiunge metodi di classe. È così che funziona. Non vedo incongruenze, solo aspettative deluse :)
Sergio Tulentsev

1
Sto lentamente sopportando il fatto che il tuo suggerimento è tanto elegante quanto la soluzione pratica di questo problema diventa. Ma mi piacerebbe sapere il motivo per cui qualcosa che funziona con le classi non funziona con i moduli.
Boris Stitnicky

6
@ BorisStitnicky Fidati di questa risposta. Questo è un idioma molto comune in Ruby, che risolve esattamente il caso d'uso che chiedi e proprio per i motivi che hai sperimentato. Può sembrare "inelegante", ma è la soluzione migliore. (Se lo fai spesso potresti spostare la includeddefinizione del metodo su un altro modulo e includerlo nel modulo principale;)
Phrogz

2
Leggi questo thread per ulteriori informazioni sul "perché?" .
Phrogz

2
@werkshy: include il modulo in una classe fittizia.
Sergio Tulentsev

47

Ecco la storia completa, che spiega i concetti di metaprogrammazione necessari per capire perché l'inclusione dei moduli funziona nel modo in cui funziona in Ruby.

Cosa succede quando viene incluso un modulo?

L'inclusione di un modulo in una classe aggiunge il modulo agli antenati della classe. Puoi guardare gli antenati di qualsiasi classe o modulo chiamando il suo ancestorsmetodo:

module M
  def foo; "foo"; end
end

class C
  include M

  def bar; "bar"; end
end

C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
#       ^ look, it's right here!

Quando chiami un metodo su un'istanza di C, Ruby esaminerà ogni elemento di questo elenco di antenati per trovare un metodo di istanza con il nome fornito. Da quando abbiamo incluso Min C, Mora è un antenato di C, quindi quando chiamiamofoo un'istanza di C, Ruby troverà quel metodo in M:

C.new.foo
#=> "foo"

Notare che l'inclusione non copia alcuna istanza o metodo di classe nella classe - aggiunge semplicemente una "nota" alla classe che dovrebbe anche cercare metodi di istanza nel modulo incluso.

E i metodi di "classe" nel nostro modulo?

Poiché l'inclusione cambia solo il modo in cui vengono inviati i metodi di istanza, l'inclusione di un modulo in una classe rende disponibili solo i metodi di istanza su quella classe. I metodi di "classe" e altre dichiarazioni nel modulo non vengono automaticamente copiate nella classe:

module M
  def instance_method
    "foo"
  end

  def self.class_method
    "bar"
  end
end

class C
  include M
end

M.class_method
#=> "bar"

C.new.instance_method
#=> "foo"

C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class

In che modo Ruby implementa i metodi di classe?

In Ruby, le classi ei moduli sono oggetti semplici: sono istanze della classe Classe Module. Ciò significa che puoi creare dinamicamente nuove classi, assegnarle a variabili, ecc .:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Anche in Ruby, hai la possibilità di definire i cosiddetti metodi singleton sugli oggetti. Questi metodi vengono aggiunti come nuovi metodi di istanza alla classe singleton speciale e nascosta dell'oggetto:

obj = Object.new

# define singleton method
def obj.foo
  "foo"
end

# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]

Ma anche classi e moduli non sono solo semplici oggetti? In effetti lo sono! Ciò significa che possono avere anche metodi singleton? Sì, lo fa! Ed è così che nascono i metodi di classe:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Oppure, il modo più comune per definire un metodo di classe è quello di utilizzare selfall'interno del blocco di definizione della classe, che si riferisce all'oggetto della classe creato:

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Come includo i metodi di classe in un modulo?

Come abbiamo appena stabilito, i metodi di classe sono in realtà solo metodi di istanza sulla classe singleton dell'oggetto classe. Questo significa che possiamo semplicemente includere un modulo nella classe singleton per aggiungere un gruppo di metodi di classe? Sì, lo fa!

module M
  def new_instance_method; "hi"; end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M
  self.singleton_class.include M::ClassMethods
end

HostKlass.new_class_method
#=> "hello"

Questa self.singleton_class.include M::ClassMethodsriga non sembra molto carina, quindi ha aggiunto Ruby Object#extend, che fa lo stesso, ovvero include un modulo nella classe singleton dell'oggetto:

class HostKlass
  include M
  extend M::ClassMethods
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ there it is!

Spostando il file extend chiamata nel modulo

Questo esempio precedente non è un codice ben strutturato, per due motivi:

  1. Ora dobbiamo chiamare entrambi include e extendnella HostClassdefinizione per includere correttamente il nostro modulo. Questo può diventare molto complicato se devi includere molti moduli simili.
  2. HostClassriferimenti diretti M::ClassMethods, che è un dettaglio di implementazione del modulo Mche HostClassnon dovrebbe essere necessario conoscere o preoccuparsi.

Allora che ne dici di questo: quando chiamiamo include sulla prima riga, in qualche modo notifichiamo al modulo che è stato incluso, e gli diamo anche il nostro oggetto classe, in modo che possa chiamare extendse stesso. In questo modo, è compito del modulo aggiungere i metodi di classe se lo desidera.

Questo è esattamente lo scopo del metodo specialeself.included . Ruby chiama automaticamente questo metodo ogni volta che il modulo viene incluso in un'altra classe (o modulo) e passa l'oggetto della classe host come primo argomento:

module M
  def new_instance_method; "hi"; end

  def self.included(base)  # `base` is `HostClass` in our case
    base.extend ClassMethods
  end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M

  def self.existing_class_method; "cool"; end
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ still there!

Ovviamente, l'aggiunta di metodi di classe non è l'unica cosa che possiamo fare self.included. Abbiamo l'oggetto classe, quindi possiamo chiamare qualsiasi altro metodo (classe) su di esso:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end

2
Risposta meravigliosa! Riuscì finalmente a capire il concetto dopo una giornata di lotta. Grazie.
Sankalp

1
Penso che questa potrebbe essere la migliore risposta scritta che abbia mai visto su SO. Grazie per l'incredibile chiarezza e per aver esteso la mia comprensione di Ruby. Se potessi regalare a questo un bonus di 100 punti lo farei!
Peter Nixey,

7

Come ha detto Sergio nei commenti, per i ragazzi che sono già in Rails (o non importa a seconda del supporto attivo ), Concernè utile qui:

require 'active_support/concern'

module Common
  extend ActiveSupport::Concern

  def instance_method
    puts "instance method here"
  end

  class_methods do
    def class_method
      puts "class method here"
    end
  end
end

class A
  include Common
end

3

Puoi avere la tua torta e mangiarla anche tu in questo modo:

module M
  def self.included(base)
    base.class_eval do # do anything you would do at class level
      def self.doit #class method
        @@fred = "Flintstone"
        "class method doit called"
      end # class method define
      def doit(str) #instance method
        @@common_var = "all instances"
        @instance_var = str
        "instance method doit called"
      end
      def get_them
        [@@common_var,@instance_var,@@fred]
      end
    end # class_eval
  end # included
end # module

class F; end
F.include M

F.doit  # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]

Se intendi aggiungere variabili di istanza e di classe, finirai per strapparti i capelli poiché ti imbatterai in un mucchio di codice rotto a meno che non lo fai in questo modo.


Ci sono alcune cose strane che non funzionano quando si passa class_eval un blocco, come la definizione di costanti, la definizione di classi annidate e l'utilizzo di variabili di classe al di fuori dei metodi. Per supportare queste cose, puoi dare a class_eval un heredoc (stringa) invece di un blocco: base.class_eval << - 'END'
Paul Donohue
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.