Building Resilient Code: Harnessing the Power of Value Objects
Value objects are a very effective tool to improve the quality of your application, as well as the readability of your code. These are small objects which contain domain-specific business rules. They're particularly useful when working with dynamically-typed languages like PHP, where the move to using more structured types has been pivotal in improving application quality.
What are value objects, and why do we need them?
Value objects are small objects which provide a guarantee of validity on the data they contain. They're immutable representations of data, which are aware of business rules. They help us to write more robust and readable code, minimising bugs. They're sometimes confused with Data Transfer Objects, which are another useful tool for ensuring data consistency within an application. At a high level, DTOs are concerned with the structure of data, while value objects are concerned with the validity of that data. We'll go into more detail on the difference between value objects and DTOs a little later on!
That's quite a mouthful, and we already have type-checking in most modern languages, so what's the benefit of adding a new concept to our stack? Perhaps an example will make it clearer!
Consider the following example, where we are trying to create a user.
class User {
public function __construct(
private string $name,
private string $email,
private string $phone
) {}
}
$user = new User('Paul', 'Conroy', 'paul@conroyp.com');
In this case, we have type checking on our parameters, but we've also got a bug sneaking in here. We may have been coding distracted or half-asleep, and thought our constructor wanted first name, last name, email. Now our $email
contains what looks like a last name, and our $phone
has an email address in it. And this, despite complying with all of the type restrictions. Oops!
// Result:
User {
-name: "Paul",
-email: "Conroy",
-phone: "paul@conroyp.com",
}
This type of bug can easily happen all over an application. Value objects can help us by putting a more formal definition on the values we work with, complaining loudly if we're not complying with the rules. This means that rather than dealing with a string which (we hope!) contains an email address, we have an Email
value object. Instead of a pair of latitude and longitude strings, we deal with a GeoPoint
, and so on.
Sounds like a lot of overhead for a relatively simple use case. Do we really need them?
Let's take an example of an application where we need to handle prices. In an application without value objects, our code may be full of functions which have to pass a combination of number (amount
) and string (currency
) around. Every function which receives these values needs to repeat validation on them to avoid errors sneaking in. Skimping on these checks makes our code look cleaner, but risks introducing bugs. This is where value objects shine.
enum Currency : string
{
case Dollar = 'USD';
case Euro = 'EUR';
}
// Immutable object, so let's make it readonly!
readonly class Price implements Stringable
{
public function __construct(
private float $amount,
private Currency $currency)
{
// Run validation check
if ($this->amount <= 0) {
throw new InvalidPriceAmountException();
}
}
public function __toString(): string
{
return number_format($this->amount, 2) . ' ' . $this->currency->value;
}
public function getAmount(): float
{
return $this->amount;
}
public function getCurrency(): string
{
return $this->currency->value;
}
}
$price = new Price(15, ‘$’); // Invalid Argument Exception
$price = new Price(0, Currency::Dollar); // InvalidPriceAmountException
$price = new Price(29.99, Currency::Dollar); // OK
echo $price; // 29.99 USD
Whenever we want to represent a price in our code, we can create a price object, and trust the internal validation done on instantiation. This means that for the lifecycle of that object in our code, we don't need to revalidate it everywhere this value is passed between functions.
// Before: requires validation every time we pass around an amount and currency
class UserBalance
{
...
function addToBalance(float $amount, string $currency) : void
{
// Validation before we can trust the values we
// received are "business-usable"
if (!currencyIsValid($currency)) {
throw new InvalidCurrencyException;
}
if ($amount <= 0) {
throw new InvalidAmountException;
}
$this->balance += $amount;
}
}
class Cart
{
function removeFromTotal(float $amount, string $currency) : void
{
// Validation needed again
if (!currencyIsValid($currency)) {
throw new InvalidCurrencyException;
}
if ($amount <= 0) {
throw new InvalidAmountException;
}
$this->total -= $amount;
}
}
...
$userBalance->addToBalance(59.99, Currency::Dollar);
$cart->removeFromTotal(19.99, Currency::Dollar);
// After: Relies on the Price value object
class UserBalance
{
...
function addToBalance(Price $price) : void
{
$this->balance += $price->getAmount();
}
}
class Cart
{
function removeFromTotal(Price $price, float $discount) : void
{
$this->total -= $price->getAmount();
}
}
...
$price = new Price(59.99, Currency::Dollar);
$userBalance->addToBalance($price);
$cart->removeFromTotal(new Price(19.99, Currency::Dollar));
This is a fairly trivial example - many more useful cases exist:
- Geo location: No more mixing up whether lat or long goes first into the 30+ different functions in your code which are concerned with positioning!
- Email address: Ensuring that strings are correctly-formatted email addresses. We could also add domain-specific blocking or allowing of certain domains. Early Facebook only allowed college domains, so in the example, an email value object could throw an exception for any domain not on the approved list.
- Phone numbers: Dealing with formatting, normalisation of spaces, and returning a consistent representation.
- Address: Handling city, state, country, zip code, and other regional levels. Handling validation, missing data, etc - a great way to centralise all the falsehoods programmers believe about addresses in one place!
Equality checks
What if we want to check if two value objects are the same? We can implement custom equality checks.
readonly class Price
{
...
public function isEqualTo(Price $other) : bool
{
return $this->getAmount() == $other->getAmount()
&& $this->getCurrency() == $other->getCurrency();
}
}
This gives us the flexibility to decide what exactly makes two objects equal.
Let's take a case where we have Person
objects, with values for name, date of birth, country of birth, and favourite colour. Perhaps people change their favourite colour frequently, so we want to exclude it from our custom check.
readonly class Person
{
public function __construct(
private string $name,
private DateTime $dob,
private string $country,
private string $favouriteColour)
...
public function isEqualTo(Person $other) : bool
{
// Ignore $favouriteColour from equality checks
return $this->getName() == $other->getName()
&& $this->getDob() == $other->getDob()
&& $this->getCountry() == $other->getCountry();
}
}
$first = new Person('Paul', new DateTime('2020-...'), 'Ireland', 'red');
$second = new Person('Paul', new DateTime('2020-...'), 'Ireland', 'green');
$first->isEqualTo($second); // true!
Or perhaps we are capturing a lot of person data from mobile phones, where capitalisation can be erratic. We want to treat paul
and Paul
the same, so need to lowercase the names before comparison.
readonly class Person
{
...
public function isEqualTo(Person $other) : bool
{
// Ignore case differences
return strtolower($this->getName()) == strtolower($other->getName())
&& ...
}
}
$first = new Person('paul', new DateTime('2...'
$second = new Person('Paul', new DateTime('2...'
$first->isEqualTo($second); // true!
Additional functionality
Value objects generally have mostly getter-type functions, but can also house some slightly more complex logic. We've seen the equals check above, and can look at a more complex example, with a DateRange
value object. This object is set up to represent the start and end of a range of dates in our application. Perhaps it's an art gallery application, and our DateRange
object is capturing the start and end dates of a particular exhibition. When a user wants to book tickets, it would be useful to know if their chosen date is during the exhibition.
readonly class DateRange
{
public function __construct(
private DateTime $start,
private DateTime $end
) {}
...
public function contains(DateTime $selected) : bool
{
return $selected >= $start && $selected <= $end;
}
}
$exhibition = new DateRange($startDate, $endDate);
$exhibition->contains($selectedDate);
What about nested value objects?
As the attributes on a value object are typed, it is possible to have value objects which contain other value objects. Consider the case of an employee, who has a name, email, and a salary. There are rules about the salary - it'll have a currency, an amount, it'll (hopefully!) be greater than zero, and may even have a bonus element. We can add business logic validation for that into our employee value object, but if we're creating salary value objects elsewhere in the application, maybe we want to extract it to it's own value object.
readonly class Salary
{
public function __construct(
private float $amount,
private string $currency,
private float $bonus
) {
// Validation...
}
}
readonly class Employee
{
public function __construct(
private string $name,
private string $email,
// Using the Salary value object directly
private Salary $salary)
...
}
$employee = new Employee('Paul', 'home@work.com', new Salary(24, 'USD', 0));
Maintenance advantages
The user of value objects within a codebase can significantly reduce the maintenance required on an application. Let's consider a scenario where our application has to handle multi-part address information, in multiple places in the codebase.
public function saveAddress($street, $city, $zipcode, $country)
...
public function sendAddressToPrint($street, $city, $zipcode, $country)
...
public function calculatePostalCostsForAddress($street, $city, $zipcode, $country)
...
// and so on...
Now let's say that there's a business requirement to change the way that addresses are represented internally. Maybe we're breaking out the house number separate to the $street
, or maybe there's a new $sublocality
between $street
and $city
levels. If we have to add in awareness of this new field, we'll have to change all of the function definitions. If these functions are currently implementing a specific interface, we'll have to make sure that is updated also, along with any other implementations. It can very quickly get very messy.
If we're already using a value object for our address, our change is a bit easier. We modify the definition of the Address
value object, but if each address-related function already accepts an Address
, we don't need to modify any of those definitions.
// Old
$address = new Address('10 Downing Street', 'London', 'SW1A 2AA', 'UK');
// New
$address = new Address('10', 'Downing Street', 'London', 'SW1A 2AA', 'UK');
public function saveAddress(Address $address)
...
public function sendAddressToPrint(Address $address)
...
public function calculatePostalCostsForAddress(Address $address)
...
When should I use a value object instead of a data transfer object?
Data Transfer Objects (DTOs) are great for sharing data between layers of an application. They define a schema that we can reliably expect to work with, as data enters or leaves our application. The data being passed is often mutable. However, DTOs are concerned only with the data types which appear in the data, rather than the business logic involved.
From wikipedia:
A DTO does not have any behavior except for storage, retrieval, serialization and deserialization of its own data (mutators, accessors, parsers and serializers). In other words, DTOs are simple objects that should not contain any business logic but may contain serialization and deserialization mechanisms for transferring data over the wire.
class EmployeeDTO
{
public function __construct(
private string $name,
private string $phone,
private string $email,
private string $salaryCurrency,
private float $salaryAmount,
....
)
}
Value objects, on the other hand, need to care about the specifics of the business domain, not just the types. They'll contain the logic which ensures that a string is an email address, for example, complaining loudly if not. Value objects also tend to be immutable.
Understanding the use cases for each, we can ensure the consistency and integrity of exchanged data, getting the best of both worlds!
class EmployeeDTO
{
public function __construct(
private string $name,
private Phone $phone,
private Email $email,
private Salary $salary,
....
)
}
Summary
Value objects can be a really useful tool for improving the quality of your application. Adding these additional objects can initially feel like a lot of overhead for relative simple data handling, but as your application grows, the additional confidence you have in their validation will help your project grow much more quickly and sustainably. Having consistent validation throughout your application helps to make testing easier, keeping the number of bugs down, and makes for much more readable code!
International PHP Conference
Munich, November 2024
In November 2024, I'll be giving a talk at the International PHP Conference in Munich, Germany. I'll be talking about the page speed quick wins available for backend developers, along with the challenges of policing dangerous drivers, the impact of TV graphics on web design, and the times when it might be better to send your dev team snowboarding for 6 months instead of writing code!
Get your ticket now and I'll see you there!