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

This article was updated on

Related post

DDD - Modelling the Domain - Modules

Software systems could become very complex with a large domain model which makes it overwhelming to see the whole picture. The concept of a Module is a design element in many programming languages (in Java they are called Packages and in C# Namespaces) to divide code for managing complexity and assisting in code reuse. Typically, programming language textbooks promote modules mainly as a code organization tool.

DDD - Modelling the Domain - Services

When designing a Domain Model the prominent modelling paradigm is objects and as a result we are mapping concepts i.e. nouns to DDD building blocks such as Entities and Value Objects. The behaviour associated to those concepts is mapped to methods using verbs. However, there are domain specific functions that we would like to define in the model but cannot be attached into an existing Entity or Value Object (Evans, 2003). Instead of forcing foreign operations into domain objects, Evans (2003) introduced the concept of Domain Services.