Value Objects
This is the first of many posts on Domain Driven Design and the many concepts described in the seminal book by Eric Evans.
The goal of this concept is to keep like functionality on immutable objects together. Let’s dive right into an example.
The Problem
Say you have the following code:
class VendingMachine def validate_coin(image) # code end def choose_item(cents, item) if can_choose_item?(cents, item) vend_item(item) else message(:insert_more_money) end end def can_choose_item?(coins, item) # code end def insert_coin(image) if validate_coin_image(image) @cents += extract_coin_value ingest_held_coin else release_coin end end def extract_coin_value(image) # returns cents end def add_to_total(cents) # code end # ... snip ... end
This is a nice bit of code, but the concept of a coin is intermingled with a vending machine. Oops.
The Solution
Let’s extract the concept of a coin into a Value Object.
class Coin attr_accessor :cents, :denomination def self.from_image(image) denomination = extract_coin_denomination_from_image(image) new(denomination) end def initialize(denomination) @denomination = denomination @cents = denomination.cents if denomination end def valid? @cents && @denomination end end
Here’s the resulting vending machine class.
class VendingMachine def choose_item(item) if can_choose_item?(item) vend_item(item) else message(:insert_more_money) end end def can_choose_item?(item) item.cost == @cents end def insert_coin(image) coin = Coin.from_image(image) if coin.valid? add_to_total(coin) ingest_held_coin else release_held_coin end end def add_to_total(coin) @cents += coin.cents end # ... snip ... end
You can see here that I’m realizing I need the concept of an Item
, which has the concept of cost encoded as cents
!
Just spitballing here, but if you were a vending machine operator, and you wanted to know if someone accidentally adds a vintage coin worth more than its denomination, you could add a year
property to Coin
and then sort out all the valuable coins to go into a separate compartment.
Clearly, this type of design thinking is infectious.
Now, you could imagine if we wanted to add giving change back to the user, it would be a bit easier to enumerate the coins. You could also add a class method to Coin that would allow you to pass in a cents value and a list of available denominations to make change.
Good Value Objects effortlessly collect related functions together, and make your life easier, not harder.
As mentioned above, Value Objects infect the code, in a good way: the next developer that comes along will see how you’re using the value object, and re-use it elsewhere. And they might think of new Value Objects to extract from the system. It’s a meme with positive benefits.
Properties
Immutability
Coin.new(:quarter).denomination = :dime # => MethodNotFound: Coin#denomination=
Comparability
Coin.new(:quarter) == Coin.new(:quarter)
Good for
- encapsulation of logic
- single responsibility
- organization
Real-world examples
- Money
- Geolocation
- Url
- Date/Time
- and the list goes on…
Conclusion
This pattern is part of what some call “Infectious Design.” [link to unwritten article here, lol]
I have no reservations about using these in any codebase. They’re a staple for me, and have always served me well. And it’s hard to go overly wild with Value Objects, as they usually have a very contained definition.
Happy Value-Object-ing!