What is a Value Object?

A Value Object is a function that receives input, applies some transformations, and outputs the results in some data structure such as a vector, a list or a data.frame.

Tip: In R, a good practice is to set the Value Object to return a

data.frame at its output. This is because often the Value Object is

called during data wrangling operations.

Functions are things (Wlaschin 2018, 149)

Intuitive examples of Value Objects are:

  • Money: Money is a useful Value Object that captures monetary value. It is a combination of Amount (numeric) and Currency (character). For instance 20 USD and 20 NZD, have different monetary value. However, we can create designated functions to add/subtract one Money Value Object from another.

  • Person: Person is a Value Object that is representing an Object in our earthly life. It is a combination of First Name (character), optionally Last Name (character), Age (numeric) or Birthdate (Date), etc.

In the context of DDD, a Value Object models a domain concept. A Value Object is an object that is identified only by its data and doesn’t have a long-lived identity.

For example, think about the diners, i.e. customers, of a Pizzeria. Associating a food order and a customer requires ephemeral customer identity. The customer name matters. It is used to identify for whom a particular Pizza was made for. The names Hilary and Bill are not equal. However, once the customer has left the Pizzeria, the customer’s existence dissipates. On the other hand, if the customer is enrolled in a restaurant loyalty program, then its unique identifier on the system is both essential and long-lived.

What does add_value_object do?

Given the Value Object name, and its domain name,

When add_value_object is called

add_value_object(name = 'Customer', domain = 'Pizza Ordering')

Then the function:

  • Creates a Value Object boilerplate with several input arguments;
  • Places the file at R/pizza_ordering-value-Customer.R; and
  • Opens the file (only in interactive mode).

Tip: You don’t need to remember the naming style for the different DDD components.

Instead, ddd takes care of naming style for all domain objects.

This way DDD file names, classes and functions are congruent with each other.

What are the main components of a Value Object?

The boilerplate code produced by add_value_object looks likes this:

R/pizza_ordering-value-Customer.R

# -------------------------------------------------------------------------
#' @title Customer Value Object
#' @return (`data.frame`)
#' @export
#' @family Pizza Ordering
#' @keywords internal
#' @noRd
Customer <- function(
    size = NA_character_,
    slices = 4L,
    toppings = list(),
    takeaway = NA
){
    tibble::tibble(
        size = size,
        slices = slices,
        toppings = toppings,
        takeaway = takeaway
    )
}

Customer <- decorators::validate_arguments(Customer)

The boilerplate code includes three key activities covered by any Value Object:

  • Assigning default values to input argument values, such as different types of NA.
  • Validating the type and value range (if applicable) of the input arguments; and
  • Organizing the input arguments in a data structure, such as a data.frame.

In addition, the Value Object include transformations on the input arguments before enframing them in a data structure. Common transformations include: base::serialize, jsonlite::toJSON, tidyr::unite, and snakecase::to_title_case to name a few.

Why should I use Value Objects in my design?

The need that Value Object serve is not new. Until now you have probably been using a degenerated version of a Value Object in different places of the software.

The simplest way to implement a Value Object in R is with a function that returns a data.frame row. Compare the following two patterns for creating a data.frame that holds a Customer data.

First, using the tibble::tibble function directly for constructing the Customer

customer <- tibble(given = "Bilbo", family = "Baggins")

Second, using a Value Object as a surrogate for constructing the Customer

Customer <- function(given = NA_character_, family = NA_character_)
{
    stopifnot(is.character(given))
    stopifnot(is.character(family))
    tibble(given = given, family = family)
}

customer <- Customer(given = "Bilbo", family = "Baggins")

Both patterns return identical objects, equal in content and structure i.e. column names and classes.

Why would one prefer using a Value Object over the direct alternative? There are three apparent qualities offered by the Value Object pattern which are not offered by the alternative:

  • Safety. Each input argument belongs to a certain type. This is guaranteed by type checking at the beginning of the function.

  • NULL Value Object. Calling the Value Object with no input arguments returns the structure of the tibble with default content, such as NULL, NA, or actual values.

  • Default values for missing input arguments. In this manner, the Value Object has a well-defined behaviour for a customer without a last name, such as Madonna and Bono.

There are more subtle qualities offered by a Value Object over direct implementation. First, it allows you to capture new knowledge about the domain. Say, you have discovered that the Pizzeria makes deliveries. In this case, the Customer details need to include an address and a phone number. With a Value Object, you can rewrite the Customer concept as:

Customer <- function(given = NA_character_, family = NA_character_, 
                     phone = NA_integer_, address = NA_character_)
{
    stopifnot(is.character(given))
    stopifnot(is.character(family))
    stopifnot(is.integer(phone))
    stopifnot(is.character(address))
    tibble(given = given, family = family, phone = phone, address = address)
}

Notice that former calls to Customer in the code base, which modelled the ordering pizza process for diners, are not impacted by the added input arguments. For example, the call of previous diner works well.

customer <- Customer(given = "Bilbo", family = "Baggins")

The Value Object ascribes NA values for the phone number and address of diners, that is customers in the restaurant.

The second subtle quality regards tests. If you write unit-test test cases against returning objects, then without a Value Object, your test expectations capture only the available knowledge at the moment of writing the test. For example, a test that validates the structure of a returning data from a function call,

test_that('calling Order$get_customer_info() returns the desired results', {
    expect_setequal(
        colnames(Order$get_customer_info()),
        c("given", "family", "phone", "address")
    )
})

can instead be written against the Customer Value Object,

test_that('calling Order$get_customer_info() returns the desired results', {
    expect_setequal(
        colnames(Order$get_customer_info()),
        colnames(Customer())
    )
})

By avoiding hard coding of column names, your tests evolve along side the code base.

How Should I use Value Objects in my design?

  1. Querying an Entity, that is by using its public query methods, should return a Value Object.

  2. Calling Value Object can happen inside another Value Object.

  3. Reusing Value Object in tests, such as unit-tests and integration-tests, is useful when the test aims to capture the structure of a returning object.

References

Wlaschin, Scott. 2018. Domain Modeling Made Functional. Pragmatic Bookshelf.