source(file.path(usethis::proj_get(), "vignettes", "_common.R")) name <- "Customer" domain <- "Pizza Ordering" path_proj <- tempfile("article-") path_script <- file.path("R", filename$value(name, domain)) path_test <- file.path("tests", "testthat", "test-" %+% filename$value(name, domain)) fs::dir_create(path_proj)
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
.
message("In R, a good practice is to set the Value Object to return a <code>data.frame</code> at its output. This is because often the Value Object is called during data wrangling operations." )
Functions are things [@Wlaschin2018, p. 149]
A light example of a Value Object is one that receives a character vector,
applies the tolower
function, and organises the character vector in a
data.frame
.
Alphabet <- function(letters = LETTERS){ stopifnot(is.character(letters)) # Value Object type check letters <- tolower(letters) # Value Object transformation data.frame(letter = letters) # Value Object return object } head(Alphabet())
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 leaves 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.
add_value_object
do?Given the Value Object name
, and its domain
name,
When add_value_object
is called
command <- "add_value_object(name = '" %+% name %+% "', domain = '" %+% domain %+% "')" withr::with_dir(path_proj, eval(parse(text=command)))
Then the function:
r path_script
; andmessage("You don't need to remember the naming style for the different DDD components. Instead, <code>ddd</code> takes care of naming style for all domain objects. This way DDD file names, classes and functions are congruent with each other." )
The boilerplate code produced by add_value_object
looks likes this:
r path_script
The boilerplate code includes three key activities covered by any Value Object:
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.
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.
Querying an Entity, that is by using its public query methods, should return a Value Object.
Calling Value Object can happen inside another Value Object.
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.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.