Writing Cleaner More Resilient Code With Value Objects
As developers, we should always be striving to learn new methods to reduce the number of bugs in our code. Some methods require us to completely re-architect our code, but sometimes there are those hidden gems that we can quickly add to our code base that will immediately help us and require just a little effort to get started.
For example, you might have a function to create a user.
class User {
public static function create(
string $first,
string $last,
string $email
): User {
// ...
}
}
Notice that we’re using type parameters. I love them because they help reduce errors and they help our IDE help us.
Then in a separate file, we can call the function.
User::create(
'scott@testcom',
'Scott',
'Keck-Warren'
);
Unfortunately, we made a mistake. Did you see the error?
We switched the parameters and the email is invalid.
Even though we use type parameters, there’s no way to prevent this. Or is there?
In this article, we’ll discuss how you can use value objects to write code that is less error-prone, more expressive, and easier to maintain.
What Are Value Objects?
At a very high level, value objects are used to represent values that have no conceptual identity of objects inside of our application domain. A value object wraps data and is distinguishable only by its properties, so two value objects with the same properties are considered equal.
Age(10) == Age(10)
Examples of potential value objects include:
* email addresses
* phone numbers
* addresses
* prices
Value objects were introduced in Domain-Driven Design (DDD). DDD is a software design approach to designing complex application domains based on input from domain experts. We can use value objects anywhere, even without using the rest of DDD.
When designing value objects, there are three main characteristics they must adhere to.
1. Structural Equality
2. Immutability. And finally,
3. Self-Validation.
Let’s discuss what each of those means.
Structural Equality
A value object differs from an entity in that it doesn’t have a concept of identity so there’s no ID column for us to compare to see if they’re equal. They are instead defined by their attributes and two value objects are considered equal if their attributes are equal, thus they’re structurally equal.
For example, if we build a `TimeSpan` value object, then a `TimeSpan` of 60 minutes would be the same thing as a `TimeSpan` of one hour. Since the underlying value is the same.
Immutability
An immutable object is an object whose state cannot be modified after it’s been created. This means that when we’re creating a value object, it will always be equal to an equivalent value object. This reduces bugs because a value won’t change during the request, especially with multiple threads of execution.
PHP 8.1 provides support for this by using the `readonly` property on our attributes.
class Age
{
public readonly int $age;
public function __construct(int $age)
{
$this->age = $age;
}
}
PHP 8.2 is going to be adding support for `readonly` classes which will help us design these (and reduce the chance we dynamically create a new attribute that isn’t `readonly`).
readonly class Age
{
public int $age;
public function __construct(int $age)
{
$this->age = $age;
}
}
If you’re working in a pre-8.1 world, when you’re implementing a value object, you just don’t create a setter to prevent external changes.
class Age
{
protected int $age;
public function __construct(int $age)
{
$this->age = $age;
}
public function getAge(): int
{
return $this->age;
}
}
Self-validation
A value object must verify the validity of its attributes when it’s created. If any of its attributes are invalid, then an exception should be raised.
For example, in our `TimeSpan` example, we can’t have negative times, so we will throw an exception to indicate that the validation has failed.
Benefits
There are numerous benefits to using value objects.
The first is cleaner code. Value objects are useful for writing clean code because instead of writing a function that requires a `string`, we can very clearly write a function that requires a `PhoneNumber` class. This makes it easier to read and easier to reason about. We also don’t need to figure out the phone format we need to pass to the function (should use dashes or parentheses or nothing?). The value object can take care of all of that for us.
public function addPhoneNumber(string $phone): void {
// ...
}
// or
public function addPhoneNumber(PhoneNumber $phone): void {
// ...
}
We also get to reduce the number of function parameters. By using value objects, we can easily replace multiple parameters with a single one. For example, if we have an amount and a currency, we can just make it a `Price`.
public function add(int $amount, int $currency): void {
// ...
}
// or
public function add(Price $price): void {
// ...
}
We also get better type safety. I love parameter types in function declarations because it makes it harder for me to shoot myself in the foot. Value objects increase type safety by distinguishing between different types of values. For example, in our `create()` function, everything was `string`, so it’s easier to swap. Having value objects prevents those swaps.
class User {
public static function create(
string $first,
string $last,
string $email
): User {
// ...
}
}
// or
class User {
public static function create(
FirstName $first,
LastName $last,
Email $email
): User {
// ...
}
}
We also get some duplicate code reduction because we’re passing around extendable classes and not basic types. We can very easily add helper functions to those classes. This allows us to put all of our common logic inside of our value object classes. For example, we might have an `isEqual()` function that compares two value objects. So for our `Price` example, we might be comparing the amount and currency in `if()` statements and by using a value object we can use a custom `isEqual()` function. We just have one function that we call and if we need to add any more additional logic to check equality on a `Price` it can be just added in a single place.
Warnings
Now of course, like everything in life, you should not abuse value objects. With value objects that the number of classes that we have to support increases significantly as we add more and more of them. We also might run into some small performance issues related to converting primitives to value objects and back. Most of the time, this isn’t an issue, but it’s something to be aware of.
Example
Let’s build an `Age` value object class.
We’re going to start with a basic class structure with a constructor and a property. We’re using PHP 8.1 so we can use the `readonly` property. This makes sure our attribute is immutable.
class Age
{
public readonly int $age;
public function __construct(int $age)
{
$this->age = $age;
}
}
Now the first requirement is that our age is between 0 and 150. We add this requirement with two exceptions that we throw if the value is outside of this range. This adds self-validation.
class Age
{
public readonly int $age;
public function __construct(int $age)
{
if ($age < 0) {
throw new AgeLessThanZero("{$age} is less than zero");
}
if ($age > 150) {
throw new AgeGreaterThanOneHundredFifty("{$age} is greater than 150");
}
$this->age = $age;
}
}
Next, we’re going to add logic to allow us to compare two `Age`s using an `isEqual()` function. This adds the structural equality requirement.
public function isEqual(Age $age): bool
{
return $this->age === $age->age;
}
What happens if we want to modify the `Age`? We might want to add a year because somebody has a birthday.
public function addYear(int $value): Age
{
return new Age($this->age + $value);
}
Notice that we’re not modifying the property directly and instead are creating a whole new version of our `Age` class. This keeps the class immutable.
What You Need to Know
Value objects are a powerful tool in the programmer’s toolbox. They allow us to reduce bugs and improve readability, and you can get started using them today.
The post Writing Cleaner More Resilient Code With Value Objects appeared first on php[architect].