将实例方法委托给类方法

发布于 2024-08-19 08:02:47 字数 693 浏览 8 评论 0原文

在 Ruby 中,假设我有一个类 Foo 来允许我对我的大量 Foo 集合进行编目。所有 Foo 都是绿色且球形的,这是自然的基本法则,因此我将类方法定义如下:

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 来尝试该类中是否有任何缺失的方法来做到这一点,但我不清楚这是我应该做的事情还是一个丑陋的黑客,或者如何安全地做到这一点(特别是当我实际上在 Rails 中的 ActiveRecord 下时,我知道它通过 method_missing 做了一些聪明有趣的事情)。

你会推荐什么?

In Ruby, suppose I have a class Foo to allow me to catalogue my large collection of Foos. It's a fundamental law of nature that all Foos are green and spherical, so I have defined class methods as follows:

class Foo
  def self.colour
    "green"
  end

  def self.is_spherical?
    true
  end
end

This lets me do

Foo.colour # "green"

but not

my_foo = Foo.new
my_foo.colour # Error!

despite the fact that my_foo is plainly green.

Obviously, I could define an instance method colour which calls self.class.colour, but that gets unwieldy if I have many such fundamental characteristics.

I can also presumably do it by defining method_missing to try the class for any missing methods, but I'm unclear whether this is something I should be doing or an ugly hack, or how to do it safely (especially as I'm actually under ActiveRecord in Rails, which I understand does some Clever Fun Stuff with method_missing).

What would you recommend?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

叹梦 2024-08-26 08:02:47

Ruby 附带的 Forwardable 模块可以很好地做到这一点:

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

The Forwardable module that comes with Ruby will do this nicely:

#!/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
罪歌 2024-08-26 08:02:47

如果它是普通的 Ruby,那么使用 Forwardable 是正确的答案

如果是 Rails,我会使用 delegate,例如

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"

If it's plain Ruby then using Forwardable is the right answer

In case it's Rails I would have used delegate, e.g.

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"
向日葵 2024-08-26 08:02:47

您可以使用模块:

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

class Foo
  extend FooProperties
  include FooProperties
end

有点难看,但比使用 method_missing 更好。我会尝试在其他答案中添加其他选项......

You could use a module:

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

class Foo
  extend FooProperties
  include FooProperties
end

A little ugly, but better than using method_missing. I'll try to put other options in other answers...

闻呓 2024-08-26 08:02:47

从设计的角度来看,我认为,即使所有 Foos 的答案都是相同的,颜色和球形?是 Foo 实例的属性,因此应该定义为实例方法而不是类方法。

然而,我可以看到在某些情况下您会想要这种行为,例如当您的系统中有 Bars 时,所有这些都是蓝色的,并且您在代码中的某个位置传递了一个类,并且想知道什么在类上调用 new 之前,实例的颜色将会改变。

另外,您是正确的,ActiveRecord 确实广泛使用 method_missing 例如用于动态查找器,因此如果您沿着这条路线走下去,您需要确保您的 method_missing 调用超类中的方法(如果它确定方法名称不是它可以自行处理的名称。

From a design perspective, I would argue that, even though the answer is the same for all Foos, colour and spherical? are properties of instances of Foo and as such should be defined as instance methods rather than class methods.

I can however see some cases where you would want this behaviour e.g. when you have Bars in your system as well all of which are blue and you are passed a class somewhere in your code and would like to know what colour an instance will be before you call new on the class.

Also, you are correct that ActiveRecord does make extensive use of method_missing e.g. for dynamic finders so if you went down that route you would need to ensure that your method_missing called the one from the superclass if it determined that the method name was not one that it could handle itself.

眼眸里的快感 2024-08-26 08:02:47

我认为最好的方法是使用 Dwemthy 的数组方法

我要查找并填写详细信息,但这是骨架

编辑:耶!在职的!

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"

I think that the best way to do this would be to use the Dwemthy's array method.

I'm going to look it up and fill in details, but here's the skeleton

EDIT: Yay! Working!

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"
冬天的雪花 2024-08-26 08:02:47

您还可以这样做:

def self.color your_args; your_expression end

define_method :color, &method(:color)

You can also do this:

def self.color your_args; your_expression end

define_method :color, &method(:color)
灼痛 2024-08-26 08:02:47

您可以定义一个直通设施:

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,但在这里它应该非常安全。

You could define a passthrough facility:

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)

I don't generally like using eval, but it should be pretty safe, here.

就此别过 2024-08-26 08:02:47

这听起来有点逃避,但实际上很少需要这样做,因为您可以同样轻松地调用 Foo.color。例外情况是,如果您有许多定义了颜色方法的类。 @var 可能是多个类之一,并且您无论如何都想显示颜色。

在这种情况下,我会问自己在哪里更多地使用该方法 - 在类上还是在模型上?它几乎总是其中之一,并且将其设为实例方法没有任何问题,即使它预计在所有实例中都是相同的。

在极少数情况下,您希望两者都可以“调用”该方法,您可以执行 @var.class.color (无需创建特殊方法)或创建一个特殊方法,如下所示:

def color
自我类别.颜色
我肯定

会避免包罗万象的(method_missing)解决方案,因为它使您不必真正考虑每个方法的用法,以及它是否属于类或实例级别。

This is going to sound like a bit of a cop out, but in practice there's rarely a need to do this, when you can call Foo.color just as easily. The exception is if you have many classes with color methods defined. @var might be one of several classes, and you want to display the color regardless.

When that's the case, I'd ask yourself where you're using the method more - on the class, or on the model? It's almost always one or the other, and there's nothing wrong with making it an instance method even though it's expected to be the same across all instances.

In the rare event you want the method "callable" by both, you can either do @var.class.color (without creating a special method) or create a special method like so:

def color
self.class.color
end

I'd definitely avoid the catch-all (method_missing) solution, because it excuses you from really considering the usage of each method, and whether it belongs at the class or instance level.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文