Value object pattern

Паттерн Value Object описывает способ представления данных, при котором объект определяется не своей идентичностью, а совокупностью значений. В такой модели важна не ссылка на объект, а его содержимое. Два экземпляра считаются равными, если совпадают все их поля, независимо от того, являются ли они одним и тем же экземпляром в памяти.

Ключевые характеристики Value Object:

  • Отсутствие уникальной идентичности
  • Неизменяемость после создания
  • Сравнение по значению, а не по ссылке
  • Операции создают новые экземпляры

Такой подход особенно важен в доменах, где данные описывают состояние, а не сущность с жизненным циклом. Временные типы, денежные значения, координаты и диапазоны естественно ложатся в эту модель.


Семантика Value Object и отличие от Entity

В доменной модели различие между Entity и Value Object определяется способом идентификации.

Entity:

  • Имеет уникальный идентификатор
  • Сохраняет преемственность во времени
  • Изменяет состояние, оставаясь тем же объектом

Value Object:

  • Не имеет идентификатора
  • Полностью определяется набором значений
  • При изменении создаётся новый объект

В контексте работы со временем идентичность теряет смысл. Дата «2026-05-25» не отличается как сущность в разных частях программы. Она либо равна другой дате, либо нет.


Иммутабельность как основа модели

Иммутабельность является фундаментом Value Object подхода. Объект после создания не изменяет внутреннее состояние.

Характерные свойства:

  • отсутствие сеттеров
  • все поля final/readonly
  • любые операции возвращают новый экземпляр

Это устраняет класс проблем:

  • побочные эффекты при передаче по ссылке
  • скрытые изменения состояния
  • необходимость защитного копирования

Иммутабельность делает поведение предсказуемым, особенно в многопоточном и асинхронном коде.


Js-joda как реализация Value Object подхода

js-joda реализует модель работы с датой и временем, основанную на принципах java.time и строгом Value Object подходе.

Основная идея библиотеки заключается в том, что все временные типы являются неизменяемыми объектами-значениями. Каждый экземпляр представляет конкретное значение времени без скрытого состояния.


Основные типы и их семантика

LocalDate

LocalDate представляет календарную дату без времени и временной зоны.

  • год, месяц, день
  • отсутствие часового компонента
  • отсутствие зависимости от часового пояса

Каждое значение является самостоятельным объектом:

import { LocalDate } from '@js-joda/core';

const d1 = LocalDate.of(2026, 5, 25);
const d2 = LocalDate.of(2026, 5, 25);

d1 и d2 равны по значению, несмотря на разные ссылки.


LocalTime

LocalTime описывает время суток без даты:

  • часы
  • минуты
  • секунды
  • наносекунды

Модель также полностью иммутабельна.

import { LocalTime } from '@js-joda/core';

const t1 = LocalTime.of(10, 30);
const t2 = LocalTime.of(10, 30);

LocalDateTime

LocalDateTime объединяет дату и время без зоны:

import { LocalDateTime } from '@js-joda/core';

const dt = LocalDateTime.of(2026, 5, 25, 10, 30);

Значение не связано с конкретным моментом времени в мире, а описывает локальное представление.


Instant

Instant представляет точку на временной оси в UTC.

  • абсолютное время
  • используется для событий и timestamp
import { Instant } from '@js-joda/core';

const i1 = Instant.now();

ZonedDateTime и ZoneId

ZonedDateTime объединяет локальную дату-время с временной зоной:

import { ZonedDateTime, ZoneId } from '@js-joda/core';

const zdt = ZonedDateTime.now(ZoneId.of('Europe/Paris'));

ZoneId сам по себе также является Value Object:

  • идентифицируется строкой зоны
  • не изменяется
  • сравнивается по значению

Duration и Period

Оба типа представляют интервалы, но на разных уровнях абстракции.

Duration — машинное время (секунды, наносекунды):

import { Duration } from '@js-joda/core';

const d = Duration.ofMinutes(90);

Period — календарные интервалы:

import { Period } from '@js-joda/core';

const p = Period.ofDays(10);

Сравнение по значению

Value Object обязан корректно реализовывать семантику равенства.

В js-joda сравнение выполняется через методы:

d1.equals(d2)

или через:

d1.isEqual(d2)

Сравнение основано исключительно на значениях полей объекта.

Пример логики:

  • совпадает год
  • совпадает месяц
  • совпадает день

При совпадении всех компонентов объекты считаются равными.


Неизменяемость операций

Любая операция над временными объектами возвращает новый экземпляр.

const date = LocalDate.of(2026, 5, 25);
const next = date.plusDays(1);

Исходный объект остаётся неизменным. Это принципиально отличает модель от изменяемых структур.

Аналогично:

  • сложение интервалов
  • вычитание времени
  • изменение зон
  • преобразования форматов

Поведение в коллекциях

Value Object корректно работает в структурах данных, основанных на равенстве.

Особенность JavaScript требует осторожности, поскольку стандартные коллекции используют ссылочную семантику:

  • Set сравнивает по ссылке
  • Map ключи сравнивает по ссылке

Поэтому Value Object используется как вычисляемое значение, а не как идентификатор.

Пример:

const set = new Set();

set.add(LocalDate.of(2026, 5, 25));
set.add(LocalDate.of(2026, 5, 25));

В результате могут присутствовать два элемента, так как ссылки различны, несмотря на равенство по значению.


Преобразования и чистые функции

Операции js-joda строятся как чистые функции:

  • отсутствует скрытое состояние
  • нет зависимости от глобальных переменных
  • результат определяется только входными значениями

Пример цепочки:

const result = LocalDate
  .of(2026, 1, 1)
  .plusMonths(2)
  .plusDays(10)
  .minusWeeks(1);

Каждый шаг создаёт новый Value Object.


Сравнение и порядок

Некоторые Value Object поддерживают упорядочивание через compareTo:

d1.compareTo(d2)

Результат:

  • отрицательное число — меньше
  • ноль — равны
  • положительное — больше

Это позволяет использовать объекты в сортировке:

const sorted = dates.sort((a, b) => a.compareTo(b));

Детерминированность модели

Value Object модель обеспечивает предсказуемость:

  • одинаковые входные данные дают одинаковый результат
  • отсутствуют скрытые изменения состояния
  • поведение не зависит от времени выполнения (кроме явного вызова clock)

В js-joda это особенно важно для:

  • тестирования временной логики
  • финансовых расчётов
  • распределённых систем
  • событийных моделей

Роль Value Object в архитектуре

В доменных системах временные типы часто становятся фундаментом модели:

  • события фиксируются через Instant
  • бизнес-логика опирается на LocalDateTime
  • отчётные периоды описываются Period

Использование Value Object устраняет необходимость ручного контроля состояния и синхронизации значений между слоями приложения.


Копирование через преобразование

Вместо изменения состояния используется преобразование:

  • добавление времени
  • изменение часового пояса
  • конвертация в другой тип

Каждое преобразование возвращает новый Value Object, сохраняя исходное значение неизменным, что делает модель линейной и легко анализируемой по цепочке операций.