Introduction

In 2015, Ralph Johnson presented at Kraków JDD with a talk titled "Twenty-one Years of Design Patterns." One of Ralph's key insights was that he didn't immediately include this pattern in the book "Design Patterns: Elements of Reusable Object-Oriented Software." This observation suggests that the Value Object is a very important element within the collection of design patterns. Jakub Pilimon and Sławomir Sobótka recommend searching for Value Objects in the code as a first step in refactoring. Additionally, Value Object is one of the core building blocks in Domain-Driven Design (DDD). In this article, we will explore the value of this design pattern.

Explanation of Value Object through Analogy

In the capitalist system we live in, money is the measure of value. Poland's currency is the złoty. The złoty consists of denominations, divided into banknotes and coins. I won’t list all the denominations, as everyone is familiar with cash and knows what it is about. When you compare two banknotes of the same denomination, for instance, 10 złoty, they represent the same value. If you exchange two banknotes of the same value, nothing changes. Both parties retain the same value they started with. This is what a Value Object is about. A Value Object is responsible for holding value and, additionally, implementing the behaviors associated with that value.

Occurrences of Value Objects

Examples of things that could be implemented as Value Objects include: your first and last name, your age, money, addresses, a book's ISBN code, your driver's license number, and many other things.

Characteristics of a Value Object

Transience

Since an object holds a value, and we can compare two objects based on their values, the specific instance of the object becomes irrelevant. I can create a representation of the value now and then create it again later. For example, I can replace an instance holding the value of 10 złoty with another instance holding the same value.

Within the context of a running program, a Value Object "lives" for a very short time. This contrasts with an Entity, whose lifecycle is monitored and whose state is often saved to a database.

Measurement, Quantification, and Description of an Entity

A Value Object primarily serves to measure, quantify, and describe an entity. Here are a few examples. Let's assume our entity is a Person. A Person has an age, which can be implemented as a Value Object class. This class would describe a particular aspect of reality—in this case, the person's age. Age itself is not an entity but is one of many descriptors of a person. Another example is a Last Name. The last name isn’t the entity itself but describes a person through their family name.

Immutability

Immutability, in the context of a Value Object, means that once created, its state should not be modified. Methods that alter the state of the object can exist in a Value Object's implementation, but only as private methods. These methods should remain hidden, and the constructor should be the only place where such a method can be used.

Whole value

The concept of "Whole Value" was first described by Ward Cunningham. Sometimes, a Value Object consists of multiple attributes that collectively describe a specific piece of reality. For example, a Money Value Object might contain two attributes: one representing the amount and the other specifying the currency of that amount. If these two variables are separated, they stop representing the concept of money, thus losing their meaning.

Another example is an address. An address might consist of attributes such as house number, apartment number, postal code, street, and city/town. If we remove some of these attributes, it no longer fully represents an address.

Replaceability

The practical significance of this characteristic for a Value Object has already been described. Let’s look at an example:

export class Gear {
    private gear: number;
  
    constructor(gear: number) {
      if (gear < 0) {
        throw new Error('Negative representation of gear');
      }
  
      this.gear = gear;
    }
  
    next(): Gear {
      return new Gear(this.gear + 1);
    }
  
    previous(): Gear {
      return new Gear(this.gear - 1);
    }
  
    equals(otherGear: Gear): boolean {
      return this.gear === otherGear.gear;
    }
  
    greaterThan(gear: Gear): boolean {
      return this.gear > gear.gear;
    }
  
    lowerOrEqualTo(gear: Gear): boolean {
      return this.gear <= gear.gear;
    }
  
    toIntValue(): number {
      return this.gear;
    }
}

and

let currentGear: Gear = new Gear(2);

currentGear = currentGear.previous();

First, we create an instance of the object where the current gear is set to the second gear. Then, for business reasons, the car slows down, so we downshift to update the current gear. We return a lower gear and replace the value in the currentGear variable.

Equality of Instances

First and foremost, since we know that a value object holds a value, and this value can be compared with another instance, we can determine whether a given value is less than or greater than, or even if two values are equal. In the Java world, we would likely implement our own equals method. Within the equals method, we can compare all attributes of a given implementation of the Value Object. The comparison can also be implemented by calculating the hash value and comparing it with another calculated hash.

Side-Effect-Free Function

This point overlaps with the topic of immutability. It’s practically the same. Refer to the implementation of the Gear class above. When we return a higher or lower gear based on the current gear, we do not modify the internal attribute that holds the current state of the object. Each time, based on the current state of the created gear object, we return a new object with a new state.

Can a Value Object Contain an Attribute that is a Value Object?

Yes, of course. We can create more complex Value Objects based on other Value Objects. Let's use the Gear class implementation and create a new Value Object that represents the minimum and maximum gear of a given gearbox, called GearRange:

import { Gear } from "./Gear";

export class GearRange {
  private minGear: Gear;
  private maxGear: Gear;

  constructor(minGear: Gear, maxGear: Gear) {
    if (minGear.greaterThan(maxGear)) {
      throw new Error("Invalid gear range");
    }

    this.minGear = minGear;
    this.maxGear = maxGear;
  }

  trim(gear: Gear) {
    if (gear.greaterThan(this.maxGear)) {
      return this.maxGear;
    } else if (gear.lowerOrEqualTo(this.minGear)) {
      return this.minGear;
    } else {
      return gear;
    }
  }
}

Sources