Andrew
Andrew

Reputation: 238687

How to wrap a Ruby array with a custom object, but support the same methods as array?

I am creating an object that acts like an array, but needs to do some data initialization before items can be added to the array.

class People
  def initialize(data_array)
    @people_array = data_array.map {|data| Person.new(data) }
  end

  def <<(data)
    @people_array << Person.new(data)
  end

  # def map(&block) ...
  # def each(&block) ...
  # etc...
end

I would like to support all the same methods that the array supports and delegate to those methods so that I don't have to rewrite all of them. What is the simplest/cleanest way to achieve this?

Upvotes: 4

Views: 1466

Answers (3)

Tamer Shlash
Tamer Shlash

Reputation: 9523

Maybe inheriting the Array class is a good solution:

class People < Array
  def initialize(data_array)
    super(data_array.map {|data| Person.new(data) })
  end

  def <<(item)
    super(Person.new(item))
  end
end

However, it has some disadvantages as mentioned in the comments and in axel's answer.

An alternative solution is composition and mixins:

class People
  include Enumerable
  
  def initialize(data_array)
    @collection = data_array.map {|data| Person.new(data) }
  end

  def each(&block)
    @collection.each(&block)
  end
  
  def <<(data)
    @collection << Person.new(data)
  end
end

However, this solution only gives you methods provided by the Enumerable module, along with the << operator.

To see what methods the Array itself defines, easily do:

(Array.instance_methods - (Enumerable.instance_methods + Object.instance_methods)).sort
 => [:&, :*, :+, :-, :<<, :[], :[]=, :assoc, :at, :bsearch, :clear, :collect!,
 :combination, :compact, :compact!, :concat, :delete, :delete_at, :delete_if, :each,
 :each_index, :empty?, :fetch, :fill, :flatten, :flatten!, :index, :insert, :join,
 :keep_if, :last, :length, :map!, :pack, :permutation, :pop, :product, :push, :rassoc,
 :reject!, :repeated_combination, :repeated_permutation, :replace, :reverse, :reverse!,
 :rindex, :rotate, :rotate!, :sample, :select!, :shift, :shuffle, :shuffle!, :size, :slice,
 :slice!, :sort!, :sort_by!, :to_ary, :transpose, :uniq, :uniq!, :unshift, :values_at, :|]

However I would consider this a dirty solution, and would encourage going with Axel's answer.

Upvotes: 3

jjk
jjk

Reputation: 536

Of course, you can always meta-hack it

 class People
      def model
        @arr ||= []
      end

      def method_missing(method_sym, *arguments, &block)
        define_method method_sym do |*arguments|
          model.send method_sym, *arguments
        end
        send(method_sym, *arguments, &block)      
      end

      #probably should implement respond_to? here
end

I like this approach a little better because you aren't confusing implementing against Array versus extending Array. You can build a custom matcher to only implement particular array methods for example that are available for your People class.

Upvotes: 1

Axel Tetzlaff
Axel Tetzlaff

Reputation: 1364

You can use a Delegator for that

class SimpleDelegator < Delegator
  def initialize(obj)
    super                  # pass obj to Delegator constructor, required
    @delegate_sd_obj = obj # store obj for future use
  end
end

I would discourage from extending Array.

Upvotes: 1

Related Questions