2010-01-28 8 views
10

In Ruby, angenommen, ich habe eine Klasse Foo, damit ich meine große Sammlung von Foos katalogisieren kann. Es ist ein Grundgesetz der Natur, die alle Foos sind grün und sphärisch, so habe ich Klassenmethoden wie folgt definiert:Delegieren von Instanzmethoden an die Klassenmethode

class Foo 
    def self.colour 
    "green" 
    end 

    def self.is_spherical? 
    true 
    end 
end 

Das bin ich

Foo.colour # "green" 

aber nicht

my_foo = Foo.new 
my_foo.colour # Error! 

lets do trotz der Tatsache, dass my_foo ist einfach grün.

Offensichtlich könnte ich eine Instanz Methode colour definieren, die self.class.colour aufruft, aber das wird unhandlich, wenn ich viele solche grundlegenden Eigenschaften habe.

Ich kann es vermutlich auch tun, indem ich method_missing definiere, um die Klasse für irgendwelche fehlenden Methoden zu versuchen, aber ich bin unklar, ob das etwas ist, was ich tun sollte oder ein hässlicher Hack oder wie man es sicher macht (besonders als ich bin eigentlich unter ActiveRecord in Rails, was ich unter Clever Fun Stuff mit method_missing verstehe.

Was würden Sie empfehlen?

+1

Ohne den colo (u) r des Bikesheds debattieren zu wollen, kann es eine gute Idee sein, amerikanisches Englisch im Code anstatt Commonwealth English zu verwenden. –

+0

Dies ist mein eigener persönlicher Code; die einzigen Leute, die es jemals sehen werden, sind ich und ein paar Freunde auch aus Großbritannien. Ich benutze American in Workplace-Code, wenn Hausstil es vorschreibt - ich werde es nicht in meinem eigenen verwenden. – Chowlett

+0

@Chowlett, das ist definitiv Ihre Wahl. Aber du wirst sie immer noch mischen, z.B. wenn Sie auf Ihre '@ colour' in einer' initialize' Methode zugreifen. – ecoologic

Antwort

2

Sie könnten eine Pass-Through-Anlage definieren:

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) 

Ich mag mit eval Regel nicht, aber es sollte ziemlich sicher, hier sein.

+0

Dies ist auch ein wirklich starker Anwärter für genau das, was ich brauche. – Chowlett

+0

Dieser wird viel besser mit Vererbung umgehen als die andere Antwort, die ich gepostet habe. –

+0

Ja. Es ist einfach, ich verstehe, was es tut, es behandelt die Vererbung, und es sieht aus wie eine Rails-artige Deklaration (has_many usw.)! – Chowlett

4

Sie könnten ein Modul verwenden:

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

class Foo 
    extend FooProperties 
    include FooProperties 
end 

Ein wenig hässlich, aber besser als method_missing verwenden. Ich werde versuchen, andere Optionen in andere Antworten zu setzen ...

+0

Schön. Kann dies die Vererbung behandeln? Das heißt, wenn Thing und Bar von Foo erben und Dinge immer "schwer" sind, während Balken immer "leicht" sind; Benötige ich separate ThingProperties- und BarProperties-Module oder kann ich sie irgendwie in FooProperties rollen? – Chowlett

1

Das wird sich wie ein bisschen wie ein Cop-out anhören, aber in der Praxis gibt es selten eine Notwendigkeit, dies zu tun, wenn Sie nur Foo.color anrufen können so leicht. Die Ausnahme ist, wenn Sie viele Klassen mit definierten Farbmethoden haben. @var ist möglicherweise eine von mehreren Klassen und Sie möchten die Farbe trotzdem anzeigen.

Wenn das der Fall ist, würde ich mich fragen, wo Sie die Methode mehr verwenden - auf der Klasse oder auf dem Modell? Es ist fast immer das eine oder das andere, und es ist nichts falsch daran, es zu einer Instanzmethode zu machen, obwohl es erwartet wird, dass es in allen Instanzen gleich ist.

In dem seltenen Fall, Sie wollen die Methode „aufrufbar“ von beiden kann man entweder @ var.class.color tun (ohne eine spezielle Methode zu schaffen) oder eine spezielle Methode erstellen wie folgt:

def Farbe self.class.color end

Ich würde auf jeden Fall die Catch-All (method_missing) Lösung vermeiden, weil es Sie davon abhält, wirklich die Verwendung jeder Methode zu betrachten und ob sie auf Klassen- oder Instanzenebene gehört.

4

Aus einer Design-Perspektive, würde ich argumentieren, dass, obwohl die Antwort für alle Foos, Farbe und Kugel gleich ist? sind Eigenschaften von Instanzen von Foo und sollten daher als Instanzmethoden und nicht als Klassenmethoden definiert werden.

Ich kann jedoch einige Fälle sehen, wo Sie dieses Verhalten z. Wenn Sie auch Bars in Ihrem System haben, sind alle blau und Sie haben eine Klasse irgendwo in Ihrem Code und möchten wissen, welche Farbe eine Instanz sein wird, bevor Sie new auf der Klasse anrufen.

Auch haben Sie Recht, dass ActiveRecord extensive Verwendung von method_missing z. Für dynamische Finder sollten Sie also sicherstellen, dass Ihr method_missing den Namen aus der Oberklasse aufruft, wenn er feststellt, dass der Methodenname nicht einer ist, den er selbst behandeln könnte.

+0

Sie haben absolut Recht, sie sind Eigenschaften der Instanz, anstatt der Klasse - aber Sie haben die Situation genagelt ich brauche diese Funktion; Ich muss eine Entscheidung in meinem Code treffen, wenn ich die Klasse in der Hand habe, bevor ich neu anrufe. – Chowlett

3

Ich denke, dass der beste Weg dies zu tun wäre, the Dwemthy's array method zu verwenden.

Ich werde es nachschlagen und in Details zu füllen, aber hier ist das Skelett

EDIT: Yay! Arbeiten!

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

Das könnte genau das sein, wonach ich suche ... Ich freue mich auf das detaillierte Update. – Chowlett

+0

@Chris: Das detaillierte Update ist fertig. Lass es mich wissen, wenn du Probleme damit hast. – rampion

+0

Derzeit wird die Farbe nicht vererbt (da sie in einer Instanzvariable der Klasse gespeichert ist), aber Sie können dies so ändern, dass die aktuellen Standardwerte übernommen werden, wenn eine Klasse unterklassifiziert wird, indem auch Thing.inherited – rampion

21

Das weiterleitbares-Modul, das mit Rubin kommt, wird dies gut tun:

#!/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

Etwas kürzer: 'def_delegator self,: color' –

+0

@Martin Carpenter, Natürlich! Redigiert und danke. –

+5

Beachten Sie, dass wenn Sie 'def_delegator' ein Symbol für das erste Argument geben, wird dies später ausgewertet. Wenn Sie 'Foo' mit' FooChild' ableiten und FooChilds Instanzen an 'FooChild' und nicht' Foo' delegieren möchten, verwenden Sie 'def delegator: self,: colour', damit' self' nicht als 'Foo' ausgewertet wird '. Es scheint wirklich ein Eval zu sein: 'def_delegator: 'puts' hi '; self'' hat den gleichen Effekt, druckt aber auf Standard out. –

2

Sie können dies auch tun:

def self.color your_args; your_expression end 

define_method :color, &method(:color) 
5

Wenn es klar Ruby dann Forwardable verwendet, ist die richtige Antwort

Wenn es Rails ist, hätte ich delegate verwendet, z

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" 
Verwandte Themen