Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best Way to Abstract Initializing Attributes

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.

like image 900
Yehuda Katz Avatar asked Jul 03 '09 06:07

Yehuda Katz


People also ask

What is the use of abstract method in it?

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.

Should I pre-initialize the class with all the attributes?

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.

What is an abstract class in Java?

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.

Is it possible to avoid using attributes in code?

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


2 Answers

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.

like image 140
molf Avatar answered Oct 24 '22 07:10

molf


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
like image 22
Simone Carletti Avatar answered Oct 24 '22 06:10

Simone Carletti