Value Objects

Value Objects help us define semi-complex, identity-less objects without us needing to resort to spaghetti code.

TL;DR

Value Objects are like non-unique Entities. You use them in much the same way, except they bear no own identity. An instance of a Value Object is equivalent to another instance if they have the same properties and values. They are excellent for containing complex creational logic and work well when combined on Entities that contain Value Objects as part of their data.

Value objects are a Godsend.

Value objects are defined by attributes, not by identity. This makes them great for cases where you want to provide a "vending machine" for non-trivial objects, such as in our case, a TimeSlot. The TimeSlot itself has no identity, but it does have unique values in non-unique keys/attributes. Because this type of object needs to always be correctly constructed, we can delegate the responsibility into (for example) a class that creates such TimeSlots. You don't pass around Value Objects that much, nor update them. Instead, you instantiate new onesβ€”they are 100% replaceable and interchangeable, after all!

This pattern is effective in refactoring, such as when wanting to cut down on primitive obsession.

Producing non-entity objects might invite one to use "easy" and regressive patterns fished out of the recesses of one's memory bank. "These aren't important!" Wrong.

Creating a TimeSlot as a Value Object

If there is something I know I need to build more often, it's Value Objects.

Get-A-Room doesn't have very many Value Objects (two, in fact). Let's look at the TimeSlot. This is how it's used:

code/Reservation/SlotReservation/src/domain/aggregates/Slot.ts
const timeSlot = new TimeSlot();
const currentTime = this.getCurrentTime();

for (let slotCount = 0; slotCount < numberHours; slotCount++) {
  const hour = startHour + slotCount;
  timeSlot.startingAt(hour);
  const { startTime, endTime } = timeSlot.get();
  const newSlot = this.makeSlot({ currentTime, startTime, endTime });
  slots.push(newSlot);
}

And the Value Object itself:

code/Reservation/SlotReservation/src/domain/valueObjects/TimeSlot.ts
import { TimeSlotDTO } from "../../interfaces/TimeSlot";

import { InvalidHourCountError } from "../../application/errors/InvalidHourCountError";

/**
 * @description Handles the creation of valid time objects.
 */
export class TimeSlot {
  private startTime = "";
  private endTime = "";

  /**
   * @description Creates a valid time object. Requires an `hour` provided as
   * a number as input for the starting hour. Assumes 24 hour clock.
   *
   * All time slots are 1 hour long and provided as ISO strings.
   * @example ```
   * const timeSlot = new TimeSlot();
   * timeSlot.startingAt(8);
   * ```
   */
  public startingAt(hour: number): void {
    if (hour > 24) throw new InvalidHourCountError();
    if (hour <= 0) hour = 0;

    const startHour = hour.toString().length === 1 ? `0${hour}` : `${hour}`;
    const endHour =
      (hour + 1).toString().length === 1 ? `0${hour + 1}` : `${hour + 1}`;
    const day = new Date(Date.now()).toISOString().substring(0, 10);
    const startTime = new Date(`${day}T${startHour}:00:00`).toISOString();
    const endTime = new Date(`${day}T${endHour}:00:00`).toISOString();

    this.startTime = startTime;
    this.endTime = endTime;
  }

  /**
   * @description Returns a `TimeSlotDTO` for the start and end time.
   */
  public get(): TimeSlotDTO {
    return {
      startTime: this.startTime,
      endTime: this.endTime,
    };
  }
}

To save on memory we are reusing the same TimeSlot instance and calling it several times throughout the loop. This is probably not the right way to do it in certain circumstances, but here I feel it makes sense as we are never relying on the instance of the Value Object itself, just asking it to return a Data Transfer Object based on the input data. Perhaps this can be seen as acceptable in the limited range of uses that we get to use TimeSlot for.

On the plus side, we are neatly encapsulating a lot of tedious detail out of the actual usage contexts. This also ensures that validation is done and that the integrity is correct and can be trusted; You'll see the error handling if we receive an hour count over 24, and how we are resetting any zero values to an acceptable base.

It should be clear that Value Objects can be as simple or complex as possible. Use them whenever you feel that unique data types or values need to be addressed in a controlled manner.

Last updated