DEV Community

MetaDave ๐Ÿ‡ช๐Ÿ‡บ
MetaDave ๐Ÿ‡ช๐Ÿ‡บ

Posted on

7 1

Ruby Value Object Instance Caching

TL:DR

Value objects with few distinct values can be cached to save memory.

Problem

We love value objects, but they're not free of system impact. Each instance uses memory, and the less of that we use the better (looking at you ActiveRecord).

Some value objects would naturally be expect to have very many distinct values (ISBN, SSN, email address), and may even be unique, but some have very few. There are only a couple of hundred countries in the world to share among your 100,000 Customer records for example, and in practice you might only be using a small number of those.

If you wanted to categorise all of your customers according to country, or an attribute of the country, Customer.map(&:country).map(&:currency).uniq, then you would probably rather not initialise 100,000 instances of the Country value object.

Solution

Provide a caching mechanism, so multiple calls to Country.new("ES") return the same instance of the Country class.

One of the principles of value objects is that they be immutable, so this is fine.

class Country
  def self.new(code)
    @instance_cache ||= {}
    @instance_cache[code] = super(code) unless @instance_cache.key?(code)
    @instance_cache.fetch(code)
  end

  def initialize(code)
    @code = code
  end
end

Other memoisation techniques might be more pleasing to an individual. Choose one that works for you.

Calling Country.new("DE") causes:

  1. @instance_cache to be set as an empty hash if it is not already defined.
  2. @instance_cache to be checked to see if "DE" is already a key โ€“ if it is not a new instance of Country is initialised and added to @instance_cache.
  3. The instance for "DE" to be read from @instance_cache and implicitly returned.

As a bonus, this is also slightly faster than instantiating a new instance.

Demonstration

Consider two value objects:

class CountryCached
  def self.new(code)
    @instance_cache ||= {}
    @instance_cache[code] = super(code) unless @instance_cache.key?(code)
    @instance_cache.fetch(code)
  end

  def initialize(code)
    @code = code
  end
end

class CountryUncached
  def initialize(code)
    @code = code
  end
end
2.4.4 :001 > class CountryCached
2.4.4 :002?>     def self.new(code)
2.4.4 :003?>         @instance_cache ||= {}
2.4.4 :004?>         @instance_cache[code] = super(code) unless @instance_cache.key?(code)
2.4.4 :005?>         @instance_cache.fetch(code)
2.4.4 :006?>       end
2.4.4 :007?>   
2.4.4 :008 >       def initialize(code)
2.4.4 :009?>         @code = code
2.4.4 :010?>       end
2.4.4 :011?>   end
 => :initialize 
2.4.4 :012 > 
2.4.4 :013 >   class CountryUncached
2.4.4 :014?>     def initialize(code)
2.4.4 :015?>         @code = code
2.4.4 :016?>       end
2.4.4 :017?>   end
 => :initialize 
2.4.4 :018 > 
2.4.4 :019 >   
2.4.4 :020 >   CountryCached.new("DE")
 => #<CountryCached:0x00007f8bff83b430 @code="DE"> 
2.4.4 :021 > CountryCached.new("DE")
 => #<CountryCached:0x00007f8bff83b430 @code="DE"> 
2.4.4 :022 > CountryCached.new("FR")
 => #<CountryCached:0x00007f8bff037ce8 @code="FR"> 
2.4.4 :023 > CountryCached.new("DE") == CountryCached.new("DE")
 => true 
2.4.4 :024 > CountryCached.new("DE") == CountryCached.new("FR")
 => false 
2.4.4 :025 > 
2.4.4 :026 >   
2.4.4 :027 >   
2.4.4 :028 >   CountryUncached.new("DE")
 => #<CountryUncached:0x00007f8bfe877a08 @code="DE"> 
2.4.4 :029 > CountryUncached.new("DE")
 => #<CountryUncached:0x00007f8bfe85f958 @code="DE"> 
2.4.4 :030 > CountryUncached.new("FR")
 => #<CountryUncached:0x00007f8bfe854670 @code="FR"> 
2.4.4 :031 > CountryUncached.new("DE") == CountryUncached.new("DE")
 => false 
2.4.4 :032 > CountryUncached.new("DE") == CountryUncached.new("FR")
 => false 

For the cached value, the object ids for the two "DE" values is identical: 0x00007f8bff83b430. For the uncached value they are different: 0x00007f8bfe877a08 and 0x00007f8bfe85f958.

Note also one of the benefits, that the objects themselves are now directly comparable:

2.4.4 :023 > CountryCached.new("DE") == CountryCached.new("DE")
 => true 

vs

2.4.4 :031 > CountryUncached.new("DE") == CountryUncached.new("DE")
 => false 

So to summarise then ...

Caching instances of value objects by overriding the class new method:

  • Is not very difficult
  • Improves memory usage
  • Makes objects directly comparable

Nice.

๐Ÿ‘‹ While you are here

Reinvent your career. Join DEV.

It takes one minute and is worth it for your career.

Get started

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

๐Ÿ‘‹ Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Communityโ€”every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple โ€œthank youโ€ goes a long wayโ€”express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay