What's the best way to abstract this pattern:
class MyClass
  attr_accessor :foo, :bar
  def initialize(foo, bar)
    @foo, @bar = foo, bar
  end
end
A good solution should take superclasses into consideration and be able to handle still being able to have an initializer to do more things. Extra points for not sacrificing performance in your solution.
It allows you to create a set of methods that must be created within any child classes built from the abstract class. A class which contains one or more abstract methods is called an abstract class. An abstract method is a method that has a declaration but does not have an implementation.
Note: one idea is to pre-initialize the class with all the attributes that one expects to encounter, e.g. Is this actually a good idea? What if I don't know what my attributes are a priori? Show activity on this post. How can I rewrite this for greater maintainability without sacrificing flexibility? You don't.
An abstract class can be considered as a blueprint for other classes. It allows you to create a set of methods that must be created within any child classes built from the abstract class. A class which contains one or more abstract methods is called an abstract class.
Not always possible or sensible, but it should the case whenever you don't have really good reasons for avoiding it. That's not a good idea. Sure, then the attribute is there, but may have a bogus value, or even a valid one that covers up for code not assigning the value (or a misspelled one).
A solution to that problem already (partially) exists, but if you want a more declarative approach in your classes then the following should work.
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
You can now do:
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
Some characteristics of this solution:
initialize_with
initialize to do custom initializationsuper in initialize
initialize are the arguments that were not consumed by attributes specified with initialize_with
initialize_with are inherited, but defining a new set on a child class will remove the parent attributesIf you want to create a solution with absolute minimal performance overhead, it would be not that difficult to refactor most of the functionality into a string which can be evaled when the initializer is defined. I have not benchmarked what the difference would be.
Note: I found that hacking new works better than hacking initialize. If you define initialize with metaprogramming, you'd probably get a scenario where you pass a block to initialize_with as a substitute initializer, and it's not possible to use super in a block.
This is the first solution that comes to my mind. There's one big downside in my module: you must define the class initialize method before including the module or it won't work.
There's probably a better solution for that problem, but this is what I wrote in less than a couple of minutes.
Also, I didn't keep performances too much into consideration. You probably can find a much better solution than me, especially talking about performances. ;)
#!/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
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With