2010-06-10 3 views
5

Si consideri la seguente estensione (il modello popolare da diversi plugin Rails nel corso degli anni):Come estendi un modulo Ruby con metodi di metaprogrammazione macro-come?

module Extension 
    def self.included(recipient) 
    recipient.extend ClassMethods 
    recipient.send :include, InstanceMethods 
    end 

    module ClassMethods 
    def macro_method 
     puts "Called macro_method within #{self.name}" 
    end 
    end 

    module InstanceMethods 
    def instance_method 
     puts "Called instance_method within #{self.object_id}" 
    end 
    end 
end 

Se si voleva esporre questo per ogni classe, è possibile effettuare le seguenti operazioni:

Object.send :include, Extension 

Ora è possibile definire qualsiasi classe e utilizzare il metodo di macro:

class FooClass 
    macro_method 
end 
#=> Called macro_method within FooClass 

e le istanze possono utilizzare i metodi di istanza:

FooClass.new.instance_method 
#=> Called instance_method within 2148182320 

Ma anche se Module.is_a?(Object), non è possibile utilizzare il metodo macro in un modulo.

module FooModule 
    macro_method 
end 
#=> undefined local variable or method `macro_method' for FooModule:Module (NameError) 

Ciò è vero anche se si include in modo esplicito l'originale Extension in Module con Module.send(:include, Extension).

Per singoli moduli è possibile includere le estensioni a mano e ottenere lo stesso effetto:

module FooModule 
    include Extension 
    macro_method 
end 
#=> Called macro_method within FooModule 

Ma come si fa ad aggiungere macro-come i metodi a tutti i moduli di Ruby?

risposta

22

Si consideri la seguente estensione (il modello popolare da diversi plugin Rails nel corso degli anni)

Questo è non un modello, e non è stato "reso popolare". È un anti-modello che era carico-citato da 1337 PHP h4X0rZ che non conoscono Ruby. Per fortuna, molte (tutte?) Istanze di questo anti-modello sono state eliminate da Rails 3, grazie alla dura parola di Yehuda Katz, Carl Lerche e gli altri. Yehuda usa addirittura lo stesso identico codice che hai postato come anti-esempio sia nei suoi recenti discorsi sulla pulizia del codice di Rails, che ha scritto an entire blog post proprio su questo anti-pattern.

Se si voleva esporre questo per ogni classe, è possibile effettuare le seguenti operazioni:

Object.send :include, Extension 

Se si desidera aggiungere alla Objectcomunque, allora perché non basta fare che:

class Object 
    def instance_method 
    puts "Called instance_method within #{inspect}" 
    end 
end 

Ma come si fa ad aggiungere metodi macro-like a tutti i moduli di Ruby?

Semplice: aggiungendoli a Module:

class Module 
    def macro_method 
    puts "Called macro_method within #{inspect}" 
    end 
end 

Tutto funziona solo:

class FooClass 
    macro_method 
end 
#=> Called macro_method within FooClass 

FooClass.new.instance_method 
#=> Called instance_method within #<FooClass:0x192abe0> 

module FooModule 
    macro_method 
end 
#=> Called macro_method within FooModule 

E 'solo 10 righe di codice contro il tuo 16, ed esattamente 0 di quelli 10 le linee sono metaprogrammazione o ganci o qualsiasi cosa anche lontanamente complicata.

L'unica differenza tra il codice e il mio è che nel codice, i mixins appaiono nella gerarchia di ereditarietà, quindi è un po 'più facile per eseguire il debug, perché in realtà vedi che qualcosa è stato aggiunto al Object. Ma questo è facilmente risolto:

module ObjectExtensions 
    def instance_method 
    puts "Called instance_method within #{inspect}" 
    end 
end 

class Object 
    include ObjectExtensions 
end 

module ModuleExtensions 
    def macro_method 
    puts "Called macro_method within #{inspect}" 
    end 
end 

class Module 
    include ModuleExtensions 
end 

Ora io sono legato con il codice a 16 linee, ma direi che il mio è più semplice di quanto il vostro, soprattutto se si considera che il vostro non funziona e né tu né io né quasi Gli utenti di StackOverflow 190000 possono capire perché.

+1

* C'è * quel post del blog. Sapevo che l'avevo visto da qualche parte di recente. Ancora curioso del perché l'altra strategia non funzioni, e non credo che sia un serio anti-pattern tanto quanto un difetto secondario, ma sono d'accordo sul fatto che fondamentalmente "non c'è motivo di escludere di comportarsi come estendere quando Ruby fornisce entrambi. " –

+3

+1 per "cargo-culted". –

+0

C'è un modo per definire "macro_method" come hai fatto, ma essere in grado di scegliere quali classi hanno accesso ad esso? –