views:

472

answers:

4

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.

+1  A: 

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
Simone Carletti
I'd pretty much reject this solution out of hand for the ActiveSupport dependency. Any dependency for this sort of thing would be undesirable, but requiring the most heinous Ruby library around just adds insult to injury.
Bob Aman
I used ActiveSupport just to take advantage of alias_method_chain but you don't need it at all. You can use alias.
Simone Carletti
+6  A: 

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:

  • Specify constructor attributes with initialize_with
  • Optionally use initialize to do custom initialization
  • Possible to call super in initialize
  • Arguments to initialize are the arguments that were not consumed by attributes specified with initialize_with
  • Easily extracted into a Module
  • Constructor attributes specified with initialize_with are inherited, but defining a new set on a child class will remove the parent attributes
  • Dynamic solution probably has performance hit

If 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.

molf
This solution looks pretty good. It seems like you would be able to call zsuper (super without parens or arguments), i.e. super instead of super(baz). I'd definitely like to see a solution that did not require define_method and could avoid using instance_variable_set. As you pointed out, this would probably require String eval.
Yehuda Katz
I'm accepting this solution even though it uses define_method. I'll submit my own answer below tomorrow for everyone's consideration.
Yehuda Katz
+2  A: 
class MyClass < Struct.new(:foo, :bar)
end
josh
The problem with this solution is that if you add an initializer with arguments, it wipes the default Struct initializer.
Yehuda Katz
You can still do stuff like: class MyClass < Struct.new(:foo, :bar) def initialize(*args) options = args.last.is_a?(Hash) ? args.pop : {} super end end # or class MyClass < Struct.new(:foo, :bar) def initialize(baz, *args) super(*args) end end
josh
I like class MyClass < Struct.new(:foo, :bar); def initialize(baz, *args) @baz = baz; super(*args) end; end
Yehuda Katz
A: 

This module allows an attrs hash as an option to new(). You can include the module in a class with inheritance, and the constructor still works.

I like this better than a list of attr values as parameters, because, particularly with inherited attrs, I wouldn't like trying to remember which param was which.

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

And you can do this:

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"
Michael Sofaer