Ruby Value Object Instance Caching
Tags: ruby, oop, value, memory
Published: 2019-01-25 08:54:19

---
title: Ruby Value Object Instance Caching
published: true
description: By caching value object instances you gain performance by saving memory, and make instances directly comparable.
tags: ruby,oop,value,memory
series: Getting the Most Out of Ruby Value Objects
---

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

```ruby
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:

```ruby
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
```

```ruby
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:
```ruby
2.4.4 :023 > CountryCached.new("DE") == CountryCached.new("DE")
 => true 
```
vs
```ruby
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.