2010-01-28 6 views
10

In Ruby, si supponga di avere una classe Foo per consentirmi di catalogare la mia vasta raccolta di Foos. Si tratta di una legge fondamentale della natura che tutti i Foos sono verdi e sferica, così ho definito metodi di classe come segue:Delegare i metodi di istanza al metodo di classe

class Foo 
    def self.colour 
    "green" 
    end 

    def self.is_spherical? 
    true 
    end 
end 

Questo mi permette di fare

Foo.colour # "green" 

ma non

my_foo = Foo.new 
my_foo.colour # Error! 

nonostante il fatto che my_foo sia chiaramente verde.

Ovviamente, potrei definire un metodo di istanza colour che chiama self.class.colour, ma che diventa ingombrante se ho molte di queste caratteristiche fondamentali.

Posso anche presumibilmente farlo definendo method_missing di provare la classe per tutti i metodi mancanti, ma io sono chiaro se questa è una cosa che dovrei fare o un brutto hack, o come farlo in modo sicuro (soprattutto perché In realtà è in ActiveRecord in Rails, che capisco fa alcune cose divertenti intelligenti con method_missing).

Cosa mi consiglia?

+1

Senza voler discutere il colo del bikehed, può essere una buona idea usare l'inglese americano nel codice, piuttosto che il Commonwealth inglese. –

+0

Questo è il mio codice personale; le uniche persone che possano vederlo sono io e alcuni amici anche dalla Gran Bretagna. Userò il codice americano sul posto di lavoro se lo stile di casa lo impone: non lo userò per conto mio. – Chowlett

+0

@Chowlett, questa è sicuramente la tua scelta. Ma li mescolerai ancora, ad es. quando accedi al tuo '@ colore' in un metodo' initialize'. – ecoologic

risposta

2

Si potrebbe definire un impianto passthrough:

module Passthrough 
    def passthrough(*methods) 
    methods.each do |method| 
     ## make sure the argument is the right type. 
     raise ArgumentError if ! method.is_a?(Symbol) 
     method_str = method.to_s 
     self.class_eval("def #{method_str}(*args) ; self.class.#{method_str}(*args) ; end") 
    end 
    end 
end 

class Foo 
    extend Passthrough 

    def self::colour ; "green" ; end 
    def self::is_spherical? ; true ; end 
    passthrough :colour, :is_spherical? 
end 

f = Foo.new 
puts(f.colour) 
puts(Foo.colour) 

Io in genere non come l'utilizzo di eval, ma dovrebbe essere abbastanza sicuro, qui.

modulo
+0

Questo è anche un contendente davvero forte per esattamente quello di cui ho bisogno. – Chowlett

+0

Questo si occuperà dell'eredità molto meglio dell'altra risposta che ho postato. –

+0

Sì. È semplice, capisco cosa sta facendo, gestisce l'ereditarietà e assomiglia molto alle dichiarazioni in stile Rails (has_many etc.)! – Chowlett

4

È possibile utilizzare un modulo:

module FooProperties 
    def colour ; "green" ; end 
    def is_spherical? ; true ; end 
end 

class Foo 
    extend FooProperties 
    include FooProperties 
end 

un po 'brutto, ma meglio che usare method_missing. Proverò a mettere altre opzioni in altre risposte ...

+0

Bello. Questo può gestire l'ereditarietà? Cioè, se Thing e Bar ereditano da Foo, e le cose sono sempre "pesanti", mentre le barre sono sempre "leggere"; avrei bisogno di moduli ThingProperties e BarProperties separati, oppure posso farlo in qualche modo in FooProperties? – Chowlett

1

Questo suonerà come un poliziotto, ma in pratica raramente c'è bisogno di farlo, quando puoi chiamare Foo.color solo altrettanto facilmente. L'eccezione è se hai molte classi con metodi di colore definiti. @var potrebbe essere una delle diverse classi e si desidera visualizzare il colore a prescindere.

Quando è il caso, mi chiedo dove stai utilizzando il metodo di più - sulla classe o sulla modella? È quasi sempre uno o l'altro, e non c'è niente di sbagliato nel renderlo un metodo di istanza, anche se dovrebbe essere lo stesso in tutte le istanze.

Nel raro caso si desidera che il metodo di "callable" da entrambi, è possibile fare @ var.class.color (senza creare un metodo speciale) o creare un metodo speciale in questo modo:

def colorare self.class.color fine

Eviterei sicuramente la soluzione catch-all (method_missing), perché ti sconsiglia di considerare davvero l'utilizzo di ciascun metodo e se appartiene al livello di classe o di istanza.

4

Dal punto di vista del design, direi che, anche se la risposta è la stessa per tutti Foos, a colori e sferici? sono proprietà delle istanze di Foo e come tali dovrebbero essere definiti come metodi di istanza piuttosto che metodi di classe.

Tuttavia, è possibile vedere alcuni casi in cui si desidera questo comportamento, ad es. quando hai il Bars nel tuo sistema e tutto ciò che è blu e sei passato una classe da qualche parte nel tuo codice e vorresti sapere di che colore sarà un'istanza prima di chiamare new sulla classe.

Inoltre, si è certi che ActiveRecord fa ampio uso di method_missing ad es. per i cercatori dinamici, quindi se sei andato giù per quella rotta avresti bisogno di assicurarti che il tuo method_missing abbia chiamato quello della superclasse se ha stabilito che il nome del metodo non era quello che poteva gestire da solo.

+0

Hai assolutamente ragione, sono proprietà dell'istanza, piuttosto che della classe - ma hai inchiodato la situazione ho bisogno di questa funzione; Ho bisogno di prendere una decisione nel mio codice quando ho la Classe in mano, prima chiamo nuova. – Chowlett

3

Penso che il modo migliore per farlo sarebbe quello di utilizzare the Dwemthy's array method.

ho intenzione di guardare in su e compilare i dettagli, ma qui è lo scheletro

EDIT: Yay! Lavoro!

class Object 
    # class where singleton methods for an object are stored 
    def metaclass 
    class<<self;self;end 
    end 
    def metaclass_eval &block 
    metaclass.instance_eval &block 
    end 
end 
module Defaults 
    def self.included(klass, defaults = []) 
    klass.metaclass_eval do 
     define_method(:add_default) do |attr_name| 
     # first, define getters and setters for the instances 
     # i.e <class>.new.<attr_name> and <class>.new.<attr_name>= 
     attr_accessor attr_name 

     # open the class's class 
     metaclass_eval do 
      # now define our getter and setters for the class 
      # i.e. <class>.<attr_name> and <class>.<attr_name>= 
      attr_accessor attr_name 
     end 

     # add to our list of defaults 
     defaults << attr_name 
     end 
     define_method(:inherited) do |subclass| 
     # make sure any defaults added to the child are stored with the child 
     # not with the parent 
     Defaults.included(subclass, defaults.dup) 
     defaults.each do |attr_name| 
      # copy the parent's current default values 
      subclass.instance_variable_set "@#{attr_name}", self.send(attr_name) 
     end 
     end 
    end 
    klass.class_eval do 
     # define an initialize method that grabs the defaults from the class to 
     # set up the initial values for those attributes 
     define_method(:initialize) do 
     defaults.each do |attr_name| 
      instance_variable_set "@#{attr_name}", self.class.send(attr_name) 
     end 
     end 
    end 
    end 
end 
class Foo 
    include Defaults 

    add_default :color 
    # you can use the setter 
    # (without `self.` it would think `color` was a local variable, 
    # not an instance method) 
    self.color = "green" 

    add_default :is_spherical 
    # or the class instance variable directly 
    @is_spherical = true 
end 

Foo.color #=> "green" 
foo1 = Foo.new 

Foo.color = "blue" 
Foo.color #=> "blue" 
foo2 = Foo.new 

foo1.color #=> "green" 
foo2.color #=> "blue" 

class Bar < Foo 
    add_defaults :texture 
    @texture = "rough" 

    # be sure to call the original initialize when overwriting it 
    alias :load_defaults :initialize 
    def initialize 
    load_defaults 
    @color = += " (default value)" 
    end 
end 

Bar.color #=> "blue" 
Bar.texture #=> "rough" 
Bar.new.color #=> "blue (default value)" 

Bar.color = "red" 
Bar.color #=> "red" 
Foo.color #=> "blue" 
+0

Questo potrebbe essere proprio quello che sto cercando ... Attendo con ansia l'aggiornamento dettagliato. – Chowlett

+0

@Chris: l'aggiornamento dettagliato è fatto. Fatemi sapere se vi imbattete in eventuali problemi con esso. – rampion

+0

attualmente il colore non è ereditato (poiché è memorizzato in una variabile di istanza della classe), ma è possibile modificarlo in modo che i valori predefiniti correnti vengano ereditati quando una classe è sottoclassata anche sovrascrivendo Thing.inherited – rampion

21

L'inoltrabile che viene fornito con Ruby farà questo bene:

#!/usr/bin/ruby1.8 

require 'forwardable' 

class Foo 

    extend Forwardable 

    def self.color 
    "green" 
    end 

    def_delegator self, :color 

    def self.is_spherical? 
    true 
    end 

    def_delegator self, :is_spherical? 

end 

p Foo.color    # "green" 
p Foo.is_spherical?  # true 
p Foo.new.color   # "green" 
p Foo.new.is_spherical? # true 
+1

Leggermente più corto: 'def_delegator self,: color' –

+0

@Martin Carpenter, naturalmente! Modificato, e grazie. –

+5

Si noti che se si fornisce a 'def_delegator' un simbolo per il suo primo argomento, esso verrà valutato successivamente. Quindi se sottoclassi 'Foo' con' FooChild', e vuoi che le istanze di FooChild deleghino a 'FooChild' e non a' Foo', usa 'def delegator: self,: colour' per mantenere' self' da valutazione come 'Foo '. Sembrerebbe davvero una valutazione: 'def_delegator:" mette 'hi'; self "avrà lo stesso effetto ma stamperà allo standard. –

2

si può anche fare questo:

def self.color your_args; your_expression end 

define_method :color, &method(:color) 
5

Se si tratta di semplice rubino quindi utilizzando Forwardable è la risposta giusta

Nel caso fosse Rails avrei usato delegate, ad es.

class Foo 
    delegate :colour, to: :class 

    def self.colour 
    "green" 
    end 
end 

irb(main):012:0> my_foo = Foo.new 
=> #<Foo:0x007f9913110d60> 
irb(main):013:0> my_foo.colour 
=> "green"