Peterdk
Peterdk

Reputation: 16015

customizing ruby .new operator

Let's say I have a class Foo and the constructor takes 2 parameters. Based on these parameters the initialize method does some heavy calculations and stores them as variables in the instance of the class. Object created.

Now I want to optimize this and create a cache of these objects. When creating a new Foo object, I want to return a existing one from the cache if the parameters match. How can I do this?

I currently have a self.new_using_cache(param1, param2), but I would love to have this integrated in the normal Foo.new(). Is this possible in any way?

I can also deduct that using .new() combined with a cache is not really syntactical correct. That would mean that the method should be called new_or_from_cache().

clarification It's not just about the heavy calculation, it's also preferred because of limiting the amount of duplicate objects. I don't want 5000 objects in memory, when I can have 50 unique ones from a cache. So I really need to customize the .new method, not just the cached values.

Upvotes: 2

Views: 1159

Answers (5)

DigitalRoss
DigitalRoss

Reputation: 146143

As you probably know you have reinvented the factory method design pattern and it's a perfectly valid solution using your name for the factory method. In fact, it's probably better to do it without redefining new if anyone else is going to have to understand it.

But, it can be done. Here is my take:

class Test

  @@cache = {}

  class << self
    alias_method :real_new, :new
  end

  def self.new p1
    o = @@cache[p1]
    if o
      s = "returning cached object"
    else
      @@cache[p1] = o = real_new(p1)
      s = "created new object"
    end
    puts "%s (%d: %x)" % [s, p1, o.object_id]
    o
  end

  def initialize p
    puts "(initialize #{p})"
  end

end
Test.new 1
Test.new 2
Test.new 1
Test.new 2
Test.new 3

And this results in:

(initialize 1)
created new object (1: 81176de0)
(initialize 2)
created new object (2: 81176d54)
returning cached object (1: 81176de0)
returning cached object (2: 81176d54)
(initialize 3)

Upvotes: 0

Alex Wayne
Alex Wayne

Reputation: 187134

class Foo
  @@cache = {}

  def self.new(value)
    if @@cache[value]
      @@cache[value]
    else
      @@cache[value] = super(value)
    end
  end

  def initialize(value)
    @value = value
  end

end

puts Foo.new(1).object_id #2148123860
puts Foo.new(2).object_id #2148123820 (different from first instance)
puts Foo.new(1).object_id #2148123860 (same as first instance)

You can actually define self.new, then call super if you actually want to use Class#new.

Also, this totally approach prevents any instantiation from ever occurring if a new instance isn't actually needed. This is die to the fact the initialize method doesn't actually make the decision.

Upvotes: 5

sawa
sawa

Reputation: 168199

Something like this?

class Foo
  @@cache = {}
  def initialize prm1, prm2
    if @@cache.key?([prm1, prm2]) then @prm1, @prm2 = @@cache[[prm1, prm2]] else
      @prm1 = ...
      @prm2 = ...
      @@cache[[prm1, prm2]] = [@prm1, @prm2]
    end
  end
end

Edited

To not create an instance when the parameters are the same as before,

class Foo
  @@cache = {}
  def self.new prm1, prm2
    return if @@cache.key?([prm1, prm2])
    @prm1 = ...
    @prm2 = ...
    @@cache[[prm1, prm2]] = [@prm1, @prm2]
    super
  end
end

p Foo.new(1, 2)
p Foo.new(3, 4)
p Foo.new(1, 2)

# => #<Foo:0x897c4f0>
# => #<Foo:0x897c478>
# => nil

Upvotes: 1

dbalatero
dbalatero

Reputation: 158

Here's a solution I came up with by defining a generic caching module. The module expects your class to implement the "retrieve_from_cache" and "store_in_cache" methods. If those methods don't exist, it doesn't attempt to do any fancy caching.

module CacheInitializer
  def new(*args)
    if respond_to?(:retrieve_from_cache) &&
        cache_hit = retrieve_from_cache(*args)
      cache_hit
    else
      object = super
      store_in_cache(object, *args) if respond_to?(:store_in_cache)
      object
    end
  end
end

class MyObject
  attr_accessor :foo, :bar
  extend CacheInitializer

  @cache = {}

  def initialize(foo, bar)
    @foo = foo
    @bar = bar
  end

  def self.retrieve_from_cache(foo, bar)
    # grab the object from the cache
    @cache[cache_key(foo, bar)]
  end

  def self.store_in_cache(object, foo, bar)
    # write back to cache
    @cache[cache_key(foo, bar)] = object
  end

private
  def self.cache_key(foo, bar)
    foo + bar
  end
end

Upvotes: 1

Michael Kohl
Michael Kohl

Reputation: 66837

You could use a class-level instance variable to store results from previous object instantiations:

class Foo
  @object_cache = {}

  def initialize(param1, param2)
    @foo1 = @object_cache[param1] || @object_cache[param1] = expensive_calculation
    @foo2 = @object_cache[param2] || @object_cache[param2] = expensive_calculation
  end

  private
  def expensive_calculation
    ...
  enf
end

Upvotes: 0

Related Questions