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

Одной из ключевых особенностей библиотеки js-joda является полная иммутабельность всех объектов даты и времени. Любой экземпляр LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Duration, Period и других типов никогда не изменяет собственное состояние после создания.

Такой подход заимствован из Java API java.time и обеспечивает предсказуемое поведение при работе с датами, временем, часовыми поясами и вычислениями.


Что такое иммутабельность

Иммутабельный объект — это объект, состояние которого нельзя изменить после создания.

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

Пример с обычным JavaScript-объектом:

const user = {
    name: 'Alex'
};

user.name = 'John';

console.log(user.name); // John

Объект изменился напрямую.

В Js-joda подобное невозможно:

const date = LocalDate.of(2025, 5, 10);

const newDate = date.plusDays(5);

console.log(date.toString());     // 2025-05-10
console.log(newDate.toString());  // 2025-05-15

Метод plusDays() не изменяет исходную дату. Вместо этого создаётся новый экземпляр.


Причины использования иммутабельности

Предсказуемость кода

Изменение объекта в одном месте программы может неожиданно повлиять на другое место, где используется тот же экземпляр.

Иммутабельность полностью устраняет подобную проблему.

const start = LocalDate.of(2025, 1, 1);

function addWeek(date) {
    return date.plusWeeks(1);
}

const result = addWeek(start);

console.log(start.toString());  // 2025-01-01
console.log(result.toString()); // 2025-01-08

Исходное значение гарантированно остаётся прежним.


Безопасность при параллельной работе

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

Хотя JavaScript традиционно работает в одном потоке, современные среды используют:

  • Web Workers
  • Worker Threads
  • асинхронные операции
  • серверные кластеры

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


Упрощение отладки

Если объект никогда не меняется, становится проще понимать поток данных.

При отладке достаточно выяснить:

  • где был создан объект;
  • где был получен новый экземпляр;
  • какие преобразования применялись.

Отсутствуют скрытые изменения состояния.


Отсутствие побочных эффектов

Побочный эффект — изменение внешнего состояния функции.

Иммутабельность делает функции чище и безопаснее.

Плохой пример с изменяемыми объектами:

function moveDate(date) {
    date.setDate(date.getDate() + 1);
}

Функция изменяет аргумент напрямую.

Эквивалент в Js-joda:

function moveDate(date) {
    return date.plusDays(1);
}

Исходный объект остаётся нетронутым.


Иммутабельность LocalDate

Тип LocalDate представляет только дату:

const date = LocalDate.of(2025, 7, 20);

Любые операции создают новый объект:

const updated = date.plusMonths(2);

console.log(date.toString());    // 2025-07-20
console.log(updated.toString()); // 2025-09-20

Это касается всех методов:

  • plusDays()
  • minusDays()
  • plusWeeks()
  • plusMonths()
  • withYear()
  • withMonth()
  • withDayOfMonth()

Работа методов with*

Методы with* создают копию объекта с изменённым значением.

const date = LocalDate.of(2025, 3, 10);

const changed = date.withYear(2030);

console.log(date.toString());    // 2025-03-10
console.log(changed.toString()); // 2030-03-10

Исходный экземпляр не затрагивается.


Иммутабельность LocalTime

Тип LocalTime также полностью неизменяем.

const time = LocalTime.of(10, 30);

const updated = time.plusHours(2);

console.log(time.toString());    // 10:30
console.log(updated.toString()); // 12:30

Невозможно изменить часы, минуты или секунды внутри существующего объекта.


Иммутабельность LocalDateTime

LocalDateTime объединяет дату и время.

const dateTime = LocalDateTime.of(2025, 5, 10, 14, 20);

const next = dateTime.plusDays(3);

console.log(dateTime.toString());
console.log(next.toString());

Результат:

2025-05-10T14:20
2025-05-13T14:20

Первоначальное значение сохраняется.


Иммутабельность ZonedDateTime

Особенно важна неизменяемость при работе с часовыми поясами.

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

const tokyo = zoned.withZoneSameInstant(
    ZoneId.of('Asia/Tokyo')
);

console.log(zoned.toString());
console.log(tokyo.toString());

Создаётся новый объект с другим часовым поясом.

Исходный экземпляр не изменяется.


Цепочки вызовов

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

const result = LocalDate.now()
    .plusMonths(1)
    .minusDays(5)
    .withDayOfMonth(1);

Каждый метод возвращает новый объект.

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


Сравнение с Date из JavaScript

Встроенный объект Date является изменяемым.

const date = new Date();

date.setFullYear(2030);

Метод изменяет существующий экземпляр.

Это приводит к множеству проблем:

  • трудно отслеживать изменения;
  • легко случайно изменить объект;
  • функции могут менять аргументы;
  • появляются скрытые зависимости.

Пример ошибки:

const original = new Date();

const copy = original;

copy.setMonth(11);

console.log(original);

Изменятся оба значения, потому что переменные указывают на один объект.

В Js-joda такое невозможно.


Работа со ссылками

Иммутабельность особенно важна при копировании ссылок.

const first = LocalDate.of(2025, 1, 1);

const second = first;

const third = second.plusDays(10);

console.log(first.toString());  // 2025-01-01
console.log(second.toString()); // 2025-01-01
console.log(third.toString());  // 2025-01-11

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


Поведение методов plus* и minus*

Методы изменения даты всегда возвращают новый экземпляр.

const date = LocalDate.of(2025, 8, 10);

const a = date.plusDays(1);
const b = date.plusMonths(1);
const c = date.minusWeeks(2);

Переменная date остаётся неизменной.


Работа с Period и Duration

Типы Period и Duration также иммутабельны.

Period

const period = Period.ofDays(5);

const updated = period.plusMonths(1);

console.log(period.toString());  // P5D
console.log(updated.toString()); // P1M5D

Duration

const duration = Duration.ofHours(2);

const changed = duration.plusMinutes(30);

console.log(duration.toString()); // PT2H
console.log(changed.toString());  // PT2H30M

Поведение в функциях

Иммутабельность делает функции безопаснее.

function calculateDeadline(date) {
    return date
        .plusWeeks(2)
        .withDayOfMonth(1);
}

const start = LocalDate.of(2025, 4, 10);

const deadline = calculateDeadline(start);

console.log(start.toString());    // 2025-04-10
console.log(deadline.toString()); // 2025-05-01

Функция не изменяет аргумент.


Функциональный стиль программирования

Js-joda хорошо сочетается с функциональным подходом.

Основные принципы функционального стиля:

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

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

Пример:

const normalize = date =>
    date.withDayOfMonth(1);

const shift = date =>
    date.plusMonths(1);

const result = shift(
    normalize(LocalDate.now())
);

Каждая функция возвращает новый объект.


Оптимизация памяти

На первый взгляд может показаться, что создание новых объектов неэффективно.

Однако современные JavaScript-движки хорошо оптимизируют короткоживущие объекты:

  • используется эффективный garbage collector;
  • многие объекты быстро удаляются;
  • движки применяют оптимизации выделения памяти.

Преимущества безопасности и предсказуемости значительно превосходят стоимость создания новых экземпляров.


Ошибки при работе с иммутабельностью

Игнорирование результата

Распространённая ошибка — забыть сохранить новый объект.

Неправильно:

let date = LocalDate.now();

date.plusDays(1);

console.log(date);

Дата не изменится.

Правильно:

let date = LocalDate.now();

date = date.plusDays(1);

console.log(date);

Ожидание мутации

Разработчики, привыкшие к Date, часто ожидают изменение текущего экземпляра.

const date = LocalDate.now();

date.minusWeeks(1);
date.plusMonths(2);

Никакие изменения не сохранятся.

Необходимо использовать результат каждого вызова.


Неизменяемость и сравнение объектов

Поскольку объекты не изменяются, сравнение становится надёжнее.

const first = LocalDate.of(2025, 1, 1);

const second = first.plusDays(1);

console.log(first.equals(second)); // false

Исходный объект гарантированно не изменился между сравнениями.


Архитектурные преимущества

Иммутабельность особенно полезна в:

  • Redux;
  • React;
  • Vue;
  • серверных приложениях;
  • системах расписаний;
  • финансовых системах;
  • календарях;
  • системах бронирования.

Причины:

  • проще отслеживать изменения состояния;
  • уменьшается количество ошибок;
  • легче тестировать код;
  • упрощается кэширование;
  • безопаснее асинхронная логика.

Практический пример обработки даты

const createdAt = LocalDateTime.now();

const publishedAt = createdAt
    .plusDays(7)
    .withHour(9)
    .withMinute(0);

console.log(createdAt.toString());
console.log(publishedAt.toString());

createdAt остаётся исходной датой создания.


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

Неизменяемые объекты значительно упрощают тесты.

test('add one week', () => {
    const source = LocalDate.of(2025, 1, 1);

    const result = source.plusWeeks(1);

    expect(source.toString())
        .toBe('2025-01-01');

    expect(result.toString())
        .toBe('2025-01-08');
});

Тесты становятся стабильнее, потому что отсутствуют скрытые изменения состояния.


Внутренний принцип работы

Каждый объект Js-joda хранит собственное состояние:

  • год;
  • месяц;
  • день;
  • часы;
  • минуты;
  • секунды;
  • наносекунды;
  • часовой пояс.

При вызове метода:

const next = date.plusDays(1);

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


Паттерн Copy-on-write

Js-joda фактически использует концепцию copy-on-write:

  1. существует исходный объект;
  2. изменение создаёт копию;
  3. модифицированная версия возвращается как новый экземпляр.

Это фундаментальный принцип библиотеки.


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

Практически весь API библиотеки построен вокруг неизменяемости.

Каждый метод:

  • возвращает новый объект;
  • не меняет исходный экземпляр;
  • безопасен для повторного использования;
  • предсказуем в цепочках вызовов.

Благодаря этому код с Js-joda получается:

  • чище;
  • безопаснее;
  • легче для сопровождения;
  • проще для тестирования;
  • устойчивее к ошибкам состояния.