Делегирование методов экземпляра методу класса - PullRequest
11 голосов
/ 28 января 2010

В Ruby, предположим, у меня есть класс Foo, который позволяет мне каталогизировать мою большую коллекцию Foos. Это фундаментальный закон природы, что все Foos являются зелеными и сферическими, поэтому я определил методы класса следующим образом:

class Foo
  def self.colour
    "green"
  end

  def self.is_spherical?
    true
  end
end

Это позволяет мне сделать

Foo.colour # "green"

но не

my_foo = Foo.new
my_foo.colour # Error!

несмотря на то, что my_foo явно зеленый.

Очевидно, я мог бы определить метод экземпляра colour, который вызывает self.class.colour, но это становится громоздким, если у меня много таких фундаментальных характеристик.

Я также могу предположительно сделать это, определив method_missing, чтобы попробовать класс для любых отсутствующих методов, но я не уверен, что это то, что я должен делать, или уродливый взлом, или как сделать это безопасно (особенно если На самом деле я нахожусь под ActiveRecord в Rails, который, как я понимаю, делает некоторые вещи Clever Fun Stuff с method_missing).

Что бы вы порекомендовали?

Ответы [ 8 ]

23 голосов
/ 09 июля 2010

Модуль Forwardable, поставляемый с Ruby, сделает это красиво:

#!/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
10 голосов
/ 22 мая 2014

Если это обычный Ruby, тогда правильный ответ

- использовать Forwardable

Если бы это был Rails, я бы использовал делегат , например,

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"
4 голосов
/ 28 января 2010

С точки зрения дизайна, я бы сказал, что, хотя ответ будет одинаковым для всех Foos, цветных и сферических? являются свойствами экземпляров Foo и поэтому должны быть определены как методы экземпляра, а не методы класса.

Однако я могу видеть некоторые случаи, когда вы хотели бы, чтобы такое поведение, например когда в вашей системе также есть Bars, и все они синие, и вам передают класс где-то в вашем коде, и вы хотели бы знать, каким будет цвет экземпляра, прежде чем вы вызовете new для класса.

Кроме того, вы правы, что ActiveRecord широко использует method_missing, например. для динамических искателей, поэтому, если вы пошли по этому маршруту, вам нужно убедиться, что ваш method_missing вызвал тот из суперкласса, если он определил, что имя метода не было тем, которое он мог обработать сам.

4 голосов
/ 28 января 2010

Вы можете использовать модуль:

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

class Foo
  extend FooProperties
  include FooProperties
end

Немного отвратительно, но лучше, чем использовать method_missing. Я постараюсь поставить другие варианты в других ответах ...

3 голосов
/ 28 января 2010

Я думаю, что лучший способ сделать это - использовать метод массива Двемти .

Я собираюсь посмотреть и заполнить детали, но вот скелет

РЕДАКТИРОВАТЬ : Yay! Рабочая!

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"
2 голосов
/ 09 декабря 2010

Вы также можете сделать это:

def self.color your_args; your_expression end

define_method :color, &method(:color)
2 голосов
/ 28 января 2010

Вы можете определить проходной объект:

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)

Мне вообще не нравится использовать eval, но здесь все должно быть довольно безопасно.

1 голос
/ 28 января 2010

Это будет звучать как коп, но на практике редко возникает необходимость сделать это, когда вы можете так же легко вызвать Foo.color. Исключение составляют случаи, когда у вас есть много классов с определенными методами цвета. @var может быть одним из нескольких классов, и вы хотите отображать цвет независимо.

В таком случае я бы спросил себя, где вы используете метод чаще - на уроке или на модели? Это почти всегда одно или другое, и нет ничего плохого в том, чтобы сделать его методом экземпляра, хотя ожидается, что он будет одинаковым во всех экземплярах.

В редких случаях, когда вы хотите, чтобы метод вызывался обоими способами, вы можете сделать @ var.class.color (без создания специального метода) или создать специальный метод, например:

def color self.class.color конец

Я бы определенно избегал решения всеобъемлющего (method_missing), потому что оно освобождает вас от реального рассмотрения использования каждого метода и его принадлежности к классу или уровню экземпляра.

...