Путешествия во времени

Еще Брукс писал, чем привлекательна профессия программиста. Мы можем с легкостью создавать и изменять целые миры и даже делать то, что невозможно в реальности.

Как я уже говорил, наша команда разрабатывает финансовое приложение для расчета комиссионных.

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

Доменная модель приложения содержит множество объектов. Центральным агрегатом является CommissionableEmployee, который содержит в себе информацию о сотруднике. Для упрощения допустим, что у сотрудника есть свойство - процент аванса. Кроме CommissionableEmployee есть еще большая куча связанных объектов, описывающих настройки расчета комиссий, я не буду сейчас вдаваться в подробности. Изначально CommissionableEmployee был обыкновенным плоским объектом с несколькими свойствами. Позже было добавлено требование, что некоторые свойства объекта должны быть зависимыми от времени, например процент аванса у сотрудника может изменяться в течение года. Это была первая волна версионности.

Для всех объектов, изменяемых во времени, была выделена постоянная (Continuity) и переменная (Version) часть. Управление версиями, такое как создание новой версии, разбиение существующей версии на две новых, слияние версий - было вынесено в базовый абстрактный класс, а все объекты, которым требовалась версионность, наследовались от него. Изменение в классе CommissionableEmployee было самым тяжелым, поскольку это центральный объект, от которого зависят практически все остальные, так что добавление версионности отняло практически месяц. Другие дались легче - меньше было зависимостей и использовался другой подход к изменениям. Если на CommissionableEmployee набросились сразу, разделили на две части и сломали весь код, который от него зависел, а затем мучительно собирали проект, мержили изменения других разработчиков, собирали снова, поднимали тесты, и так без конца, то с другими объектами делали проще - создавали версионный объект рядом с предыдущим, например Account2 и AccountVersion вместо Account и постепенно, один за другим, маленькими шагами, заменяли использование Account на Account2. В конце, когда никто не пользовался классом Account, его удалили, а Account2 переименовали в Account.

Спустя некоторое время после завершения перехода на версионные объекты, возникло требование о ведении аудита. Необходимо было отслеживать изменения во всех версионных объектах - кто, когда, что именно поменял, как объект выглядел до изменения и как - после. Задача не такая простая, учитывая, что структура объектов может быть совершенно произвольная, сколь угодно сложная. Существующая модель этого не позволяла. Можно было сделать изменения, затрагивающие прошлые периоды, например, изменить сотруднику процент аванса в уже заплаченном месяце, а потом пойди докажи, почему на тот момент ему была рассчитана зарплата именно так, как она была рассчитана.

Для решения этой задачи был использован паттерн Time Travel. Этот паттерн позволяет восстановить состояние объекта на любой момент в прошлом. Как побочный эффект сохраняется история всех изменений, выполняющая роль аудитных записей.



К примеру, в начале администратор решает, что некий сотрудник может иметь три версии, каждая из которых хранит разные значения процента его аванса:

  • 40% с Января по Март

  • 30% с Мая по Июнь

  • 50% с Июля до конца времен



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

В нашем случае будет последовательно создано три транзакции:

  • 1. 40% с эффективным периодом Январь - конец времен

  • 2. 30% с эффективным периодом Май - конец времен

  • 3. 50% с эффективным периодом Июль - конец времен



Затем, по прошествии какого-то времени, скажем, 10 Июня в 14:45, администратор обнаруживает ошибку. Оказывается, сотрудник должен иметь аванс 50% начиная с Апреля.
Он создает новую транзакцию

  • 4. 50% с эффективным периодом Апрель - конец времен


которая частично перекрывает 1-ю транзакцию и полностью отменяет 2-ю и 3-ю. С этого момента, то есть начиная с 10 Июня 14:45, программа будет показывать для сотрудника только 2 версии - с 40% и 50% соответственно. Если мы пересчитаем его зарплату на Май, задним числом, то она посчитается с авансом 50%. Но если возникнет вопрос, почему ранее сотруднику посчиталась зарплата на Май с авансом 30%, мы всегда сможем откатиться на момент времени, предшествующий 4-й транзакции, скажем на 9 Июня, и показать настройки сотрудника на тот момент.

Если сотрудник увольняется с Сентября, администратор создает транзакцию на увольнение (5), эффективную с Сентября по конец времен.

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

Паттерн Time Travel оказался очень полезным в применении к нашей задаче. Если статья вызовет интерес, я могу подробнее рассказать о деталях имплементации этого паттерна.

Ссылки:

Time Travel: A Pattern Language for Values That Change
Massimo Arnoldi, LifeWare
Kent Beck, Daedalos Consulting
Markus Bieri, LifeWare
Manfred Lange, Hewlett-Packard


/sites/default/files/m/2/5/8/TimeTravel.pdf
Para obter mais informações sobre otimizações de compiladores, consulte Aviso sobre otimizações.