Подтвердить что ты не робот

Лучший способ абстрактных атрибутов инициализации

Какой лучший способ абстрагировать этот шаблон:

class MyClass
  attr_accessor :foo, :bar

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

Хорошее решение должно принимать во внимание суперклассы и, чтобы справляться с тем, чтобы иметь возможность инициализировать больше вещей. Дополнительные очки за то, что вы не пожертвовали своим решением.

4b9b3361

Ответ 1

Решение этой проблемы уже (частично) существует, но если вы хотите использовать более декларативный подход в своих классах, то следующее должно работать.

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 ed при определении инициализатора. Я не оценил, какая разница.

Примечание. Я обнаружил, что взлом new работает лучше, чем взлом initialize. Если вы определяете initialize с метапрограммированием, вы, вероятно, получите сценарий, в котором вы передадите блок в initialize_with в качестве заменяющего инициализатора, и в блоке использовать super невозможно.

Ответ 2

Это первое решение, которое приходит мне на ум. Там один большой недостаток в моем модуле: вы должны определить метод инициализации класса, прежде чем включать модуль, или он не будет работать.

Вероятно, лучшее решение для этой проблемы, но это то, что я написал менее чем за пару минут.

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

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

Ответ 3

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

Ответ 4

Я знаю, что это старый вопрос с вполне приемлемыми ответами, но я хотел опубликовать свое решение, поскольку он использует преимущества 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. Все остальные нетронутые.

Ответ 5

Этот модуль позволяет использовать хэш attrs как вариант для new(). Вы можете включить модуль в класс с наследованием, и конструктор все еще работает.

Мне нравится это лучше, чем список значений attr в качестве параметров, потому что, особенно с унаследованными attrs, мне не хотелось бы пытаться запомнить, какой параметр был.

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"