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:
-
@instance_cache
to be set as an empty hash if it is not already defined. -
@instance_cache
to be checked to see if "DE" is already a key – if it is not a new instance ofCountry
is initialised and added to@instance_cache
. - 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.
Top comments (0)