DDD - Modelling the Domain - Value Objects
There are elements of the Domain Model without any conceptual identity which are typically used to characterise Entities (Evans, 2003). Those elements are called Value Objects and their significance is often neglected (Vernon, 2013). Examples of Value Objects are: Zip Code, Order Number, Phone Number, Money, Currency, Date, Date Range, E-mail Address, URL etc. Value Objects express domain concepts and are part of the Ubiquitous Language (Evans 2003; Vernon, 2013).
The absence of identity means that we can only distinguish Value Objects by the equality of their individual attributes (Fowler, 2002). Another important characteristic of Value Objects is that their state never changes i.e. they are immutable which permits their sharing among many entities (Evans 2003; Vernon, 2013).
Finally, the role of Value Objects is not only to characterise entities but in addition to offload them from computational complexity e.g. an E-mail Address has format limitations and its validity could be enforced by its constructor and not during assignment to a User Entity having an E-mail (Fowler, 2002; Evans, 2003).
For instance, the concept of Money is a representation of monetary value that could be expressed in code using a primitive data type (Hu and Peng, 2007). However, the concept is only implicitly defined and the fact that the code deals with money is only apparent by the variable or method name (Hu and Peng, 2007). The lack of explicitly defining a Money object causes problems, especially when the concept of Currency needs to be introduced (Fowler, 2002). Money and Currency are value objects that can be represented as:
NOTE! The following code is only a means to demonstrate the concept of a Value Object. It is not tested extensively, has many limitations (e.g. no multiplication/division) and the precision could be very low for your use case.
import decimal
class Currency(object):
_MAX_LENGTH = 3
def __init__(self, code):
"""A unit of exchange
:param code: 3 letter code based on ISO 4217
:type code: str
"""
if not self._is_valid(code):
raise ValueError('Code "{}" is not valid'.format(code))
self._code = code.upper()
def __str__(self):
return self._code
def __eq__(self, other):
if not isinstance(other, Currency):
raise TypeError('{} is not Currency'.format(other))
return self._code == other._code
def _is_valid(self, code):
return isinstance(code, str) and len(code) == self._MAX_LENGTH
class Money(object):
_precision = decimal.Decimal('.01')
def __init__(self, amount, currency):
"""A representation of monetary value
:param amount: the amount of money
:type amount: str, int, float, Decimal
:param currency: the unit of exchange
:type currency: Currency
"""
try:
self._amount = decimal.Decimal(amount)
except ValueError:
raise ValueError('Amount "{}" is not valid'.format(amount))
if not isinstance(currency, Currency):
raise ValueError('Currency "{}" is not a valid'.format(currency))
self._currency = currency
def __str__(self):
return '{0} {1}'.format(self._rounded_amount, self._currency)
def __eq__(self, other):
self._check_other(other)
return self._amount == other._amount
def __lt__(self, other):
self._check_other(other)
return self._amount < other._amount
def __gt__(self, other):
self._check_other(other)
return self._amount > other._amount
def __add__(self, other):
self._check_other(other)
return Money(self._amount + other._amount, self._currency)
def __sub__(self, other):
self._check_other(other)
return Money(self._amount - other._amount, self._currency)
@property
def _rounded_amount(self):
return self._amount.quantize(self._precision, decimal.ROUND_HALF_UP)
def _check_other(self, other):
if not isinstance(other, Money):
raise TypeError('{} is not Money'.format(other))
if not (self._currency == other._currency):
raise TypeError('Currency mismatch.')
Obviously, we have to write a lot of code in order to represent those concepts into the domain model as value objects. However, we offload the client from logic related to comparing and manipulating money of a particular currency. Notice that although Money objects are immutable we can add them and subtract them, resulting in an new instance of Money. The following lines demonstrate how currency and money are used in the domain model.
dollars = Currency('USD')
euros = Currency('EUR')
m1 = Money(10, dollars)
m2 = Money(10, dollars)
m3 = Money(30, euros)
print('m1 equals m2: {}'.format(m1 == m2))
result1 = m1 + m2
print('result1 is an instance of Money: {}'.format(isinstance(result1, Money)))
# the following raises an exception
result = m1 + m3
And the output is
m1 equals m2: True
result1 is an instance of Money: True
Traceback (most recent call last):
File "money.py", line 89, in <module>
result = m1 + m3
File "money.py", line 61, in __add__
self._check_other(other)
File "money.py", line 72, in _check_other
raise TypeError('Currency mismatch.')
TypeError: Currency mismatch.
References
- Evans, E. (2003) Domain-Driven Design: Tacking Complexity In the Heart of Software. Boston: Addison-Wesley.
- Fowler, M. (2002) Patterns of Enterprise Application Architecture. Boston: Addison-Wesley.
- Hu, Y. & Peng, S. (2007) 'So we thought we knew money'. ACM SIGPLAN Object Oriented Programming Systems and Applications Conference 2007. 21-25 October. Montreal: OOPSLA: pp. 971-975.
- Vernon, V. (2013) Implementing Domain-Driven Design. Boston: Addison-Wesley.
This post is an excerpt of my MSc Thesis titled "Applying Domain-Driven Design in Python - designing a self-hosted Read-it-Later service" (January 2014).