views:

439

answers:

4

I have an object now:

class Items
  attr_accessor :item_id, :name, :description, :rating

  def initialize(options = {})
      options.each {
        |k,v|
        self.send( "#{k.to_s}=".intern, v)
      }
  end

end

I have it being assigned as individual objects into an array...

@result = []

some loop>>
   @result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>

But instead of assigning my singular object to an array... how could I make the object itself a collection?

Basically want to have the object in such a way so that I can define methods such as

def self.names
   @items.each do |item|
      item.name
   end
end

I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.

A: 

The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.

Add 'return self' as the last line of initialize and you're golden.

Class Item
  def initialize(options = {})
    ## Do all kinds of stuff. 

    return self
  end
end
Ariejan
well, that part actually works ok, its just that i'm feeding my objects into a array when i'd like to have a mega object (collection) that I can perform functions
holden
Item.new always returns the newly created object regardless of the return value of initialize(). Adding return self makes no difference at all.
molf
Unlike other languages, a Ruby initializer's return value is ignored and the object created is always returned. This can be altered by re-writing the Class new method, but that is probably a bad idea.
tadman
+2  A: 

Do you know the Ruby key word yield?

I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:

class Items
  @items = []
  class << self
    attr_accessor :items
  end
  attr_accessor :name, :description
  def self.each(&args)
    @items.each(&args)
  end
  def initialize(name, description)
    @name, @description = name, description
    Items.items << self
  end
  def each(&block)
    yield name
    yield description
  end
end

a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}

This outputs

mug
cup

mug
a big cup

Did you ask for something like Items.each or a.each or for something completely different?

Whoever
It seems odd here using two yields when one with two arguments would probably be more useful.
tadman
Depends on what one wants to do. The method "each" typically calls the given block once for each element inserting the current element. Of course you are right in case of my example (which doesn't make too much sense, I just wanted to show the usage). Yielding both arguments would call the block only once inserting two elements.
Whoever
+3  A: 

A few observations before I post an example of how to rework that.

  • Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
  • Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.

One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:

class Item
  attr_accessor :item_id, :name, :description, :rating

  def initialize(options = { })
    options.each do |k,v|
      method = :"#{k}="

      # Check that the method call is valid before making it
      if (respond_to?(method))
        self.send(method, v)
      else
        # If not, produce a meaningful error
        raise "Unknown attribute #{k}"
      end
    end
  end
end

class ItemsCollection < Array
  # This collection does everything an Array does, plus
  # you can add utility methods like names.

  def names
    collect do |i|
      i.name
    end
  end
end

# Example

# Create a custom collection
items = ItemsCollection.new

# Build a few basic examples
[
  {
    :item_id => 1,
    :name => 'Fastball',
    :description => 'Faster than a slowball',
    :rating => 2
  },
  {
    :item_id => 2,
    :name => 'Jack of Nines',
    :description => 'Hypothetical playing card',
    :rating => 3
  },
  {
    :item_id => 3,
    :name => 'Ruby Book',
    :description => 'A book made entirely of precious gems',
    :rating => 1
  }
].each do |example|
  items << Item.new(example)
end

puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
tadman
is there a way to make it so that all of the attributes of Items automatically behave likes the 'names' method? IE. calling .descriptions returns the same as .names...?
holden
If you want to get adventuresome, you could either build these methods dynamically, or use method_missing to catch calls.
tadman
Can I use Ruby mixins to add such functionality to all Arrays that contain ActiveRecord objects? Some pointers will be useful.
Sanjay
You can extend the base Array class as much as you like, though that sort of thing may be a little unorthodox for specific requirements like "names". If you use the method_missing approach, though, you might be able to come up with a convention such as "collect_Xs" which collects all :x results.
tadman
+1  A: 

Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by

def method_missing(symbol_s, *arguments)
  symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
  if s == 's' and arguments.empty?
    select do |i|
      i.respond_to?(symbol) && i.instance_variables.include?("@#{symbol}")
    end.map {|i| i.send(symbol)}
  else
    super
  end
end

For his example data you will get following outputs:

puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems

As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.

Whoever
I just saw this down here after adding my comment above. If you're using Rails you can use the singularize method to do the depluralization for you instead of resorting to 's'-stripping.
tadman