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

StackOverflow https://stackoverflow.com/questions/1077949

  •  21-08-2019
  •  | 
  •  

Вопрос

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

class MyClass
  attr_accessor :foo, :bar

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

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

Это было полезно?

Решение

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

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 наследуются, но определение нового набора в дочернем классе приведет к удалению родительских атрибутов.
  • Динамическое решение, вероятно, снижает производительность

Если вы хотите создать решение с абсолютно минимальными затратами на производительность, провести рефакторинг будет не так уж и сложно. большинство функциональности в строку, которая может быть evaled, когда инициализатор определен.Я не сравнивал, в чем будет разница.

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

Другие советы

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

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

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

#!/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
class MyClass < Struct.new(:foo, :bar)
end

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

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

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

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"
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top