Лучший способ абстрактной инициализации атрибутов
-
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
наследуются, но определение нового набора в дочернем классе приведет к удалению родительских атрибутов. - Динамическое решение, вероятно, снижает производительность
Если вы хотите создать решение с абсолютно минимальными затратами на производительность, провести рефакторинг будет не так уж и сложно. большинство функциональности в строку, которая может быть eval
ed, когда инициализатор определен.Я не сравнивал, в чем будет разница.
Примечание:Я нашел этот взлом 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"