抽象初始化属性的最佳方法

时间:2009-07-03 06:08:16

标签: ruby abstraction

抽象这种模式的最佳方法是什么:

class MyClass
  attr_accessor :foo, :bar

  def initialize(foo, bar)
    @foo, @bar = foo, bar
  end
end

一个好的解决方案应该考虑超类能够处理仍然能够有一个初始化器来做更多的事情。在解决方案中不牺牲性能的额外要点。

5 个答案:

答案 0 :(得分:7)

该问题的解决方案已经(部分)exists,但如果您想在类中使用更具声明性的方法,则以下内容应该有效。

class Class
  def initialize_with(*attrs, &block)
    attrs.each do |attr|
      attr_accessor attr
    end
    (class << self; self; end).send :define_method, :new do |*args|
      obj = allocate
      init_args, surplus_args = args[0...attrs.size], args[attrs.size..-1]
      attrs.zip(init_args) do |attr, arg|
        obj.instance_variable_set "@#{attr}", arg
      end
      obj.send :initialize, *surplus_args
      obj
    end
  end
end

您现在可以:

class MyClass < ParentClass
  initialize_with :foo, :bar
  def initialize(baz)
    @initialized = true
    super(baz) # pass any arguments to initializer of superclass
  end
end
my_obj = MyClass.new "foo", "bar", "baz"
my_obj.foo #=> "foo"
my_obj.bar #=> "bar"
my_obj.instance_variable_get(:@initialized) #=> true

此解决方案的一些特征:

  • 使用initialize_with
  • 指定构造函数属性
  • (可选)使用initialize进行自定义初始化
  • 可以在super
  • 中致电initialize
  • initialize的参数是initialize_with指定的属性未使用的参数
  • 轻松提取到模块
  • 继承了initialize_with指定的构造函数属性,但在子类上定义新集将删除父属性
  • 动态解决方案可能会有性能损失

如果要创建具有绝对最小性能开销的解决方案,将大多数功能重构为字符串并不困难,该字符串在初始化程序时可以eval被定义为。我没有确定差异的基准。

注意:我发现黑客new比黑客initialize效果更好。如果使用元编程定义initialize,您可能会遇到将块作为替代初始化程序传递给initialize_with的情况,并且无法在块中使用super。 / p>

答案 1 :(得分:1)

这是我想到的第一个解决方案。我的模块有一个很大的缺点:你必须在包含模块之前定义类初始化方法,否则它将无效。

对于这个问题,可能有更好的解决方案,但这是我在不到几分钟的时间里写的。

另外,我并没有过多地考虑表演。你可能找到比我更好的解决方案,特别是谈论表演。 ;)

#!/usr/bin/env ruby -wKU

require 'rubygems'
require 'activesupport'


module Initializable

  def self.included(base)
    base.class_eval do
      extend  ClassMethods
      include InstanceMethods
      alias_method_chain :initialize, :attributes
      class_inheritable_array :attr_initializable
    end
  end

  module ClassMethods

    def attr_initialized(*attrs)
      attrs.flatten.each do |attr|
        attr_accessor attr
      end
      self.attr_initializable = attrs.flatten
    end

  end

  module InstanceMethods

    def initialize_with_attributes(*args)
      values = args.dup
      self.attr_initializable.each do |attr|
        self.send(:"#{attr}=", values.shift)
      end
      initialize_without_attributes(values)
    end

  end

end


class MyClass1
  attr_accessor :foo, :bar

  def initialize(foo, bar)
    @foo, @bar = foo, bar
  end
end

class MyClass2

  def initialize(*args)
  end

  include Initializable

  attr_initialized :foo, :bar
end


if $0 == __FILE__
  require 'test/unit'

  class InitializableTest < Test::Unit::TestCase

    def test_equality
      assert_equal MyClass1.new("foo1", "bar1").foo, MyClass2.new("foo1", "bar1").foo
      assert_equal MyClass1.new("foo1", "bar1").bar, MyClass2.new("foo1", "bar1").bar
    end

  end
end

答案 2 :(得分:1)

class MyClass < Struct.new(:foo, :bar)
end

答案 3 :(得分:1)

我知道这是一个老问题,答案完全可以接受,但我想发布我的解决方案,因为它利用了Module#prepend(Ruby 2.2中的新功能)以及模块也是非常简单的解决方案的类。首先是制造魔术的模块:

class InitializeWith < Module
  def initialize *attrs
    super() do
      define_method :initialize do |*args|
        attrs.each { |attr| instance_variable_set "@#{attr}", args.shift }
        super *args
      end
    end
  end
end

现在让我们使用我们的花哨模块:

class MyClass
  prepend InitializeWith.new :foo, :bar
end

请注意,我离开了attr_accessible这些东西,因为我认为这是一个单独的问题,尽管支持是微不足道的。现在我可以创建一个实例:

MyClass.new 'baz', 'boo'

我仍然可以为自定义初始化定义initialize。如果我的自定义initialize接受一个参数,那么这些将是为新实例提供的任何额外参数。所以:

class MyClass
  prepend InitializeWith.new :foo, :bar

  def initialize extra
    puts extra
  end
end
MyClass.new 'baz', 'boo', 'dog'

在上面的示例中@foo='baz'@bar='boo',它将打印dog

我对此解决方案的喜爱之处在于它不会使用DSL污染全局命名空间。想要此功能的对象可以prepend。其他人都没有受到影响。

答案 4 :(得分:0)

此模块允许将attrs哈希作为new()的选项。您可以将该模块包含在具有继承的类中,并且构造函数仍然有效。

我喜欢这个比作为参数的attr值列表更好,因为,特别是对于继承的attrs,我不想尝试记住哪个param是哪个。

module Attrize
  def initialize(*args)
    arg = args.select{|a| a.is_a?(Hash) && a[:attrs]}
    if arg
      arg[0][:attrs].each do |key, value|
        self.class.class_eval{attr_accessor(key)} unless respond_to?(key)
        send(key.to_s + '=', value)
      end
      args.delete(arg[0])
    end
    (args == []) ? super : super(*args)
  end
end

class Hue
  def initialize(transparent)
    puts "I'm transparent" if transparent
  end
end

class Color < Hue
  include Attrize
  def initialize(color, *args)
    p color
    super(*args)
    p "My style is " + @style if @style
  end
end

你可以这样做:

irb(main):001:0> require 'attrize'
=> true
irb(main):002:0> c = Color.new("blue", false)
"blue"
=> #<Color:0x201df4>
irb(main):003:0>  c = Color.new("blue", true, :attrs => {:style => 'electric'})
"blue"
I'm transparent
"My style is electric"