Двух видов транзакций достаточно: как мы упростили модель данных

модель данных

Ранняя версия HiBill умела записывать движение денег четырьмя разными способами: расход, заём, возврат и расчёт. У каждого была своя таблица, свой экран и свои правила.

Сегодня способов два. Рассказываем почему — и чему мы научились, пока их разбирали.

Три сущности, которые по сути были одной

Посмотрите, что на самом деле хранили три из четырёх.

Заём: деньги перешли от A к B, и теперь B должен A.

Возврат: деньги перешли от B к A и погасили часть того, что B был должен.

Расчёт: деньги перешли от B к A и погасили часть того, что B был должен, — и мы помечали его как окончательный.

Три названия. Одно событие. Деньги перешли ровно от одного человека ровно к одному. Все различия были смысловыми — в том, что пользователь имел в виду, — а не структурными. А мы зашили этот смысл в схему, где он застыл тремя ветками кода, которые пришлось бы вечно держать согласованными.

Выдало всё поле статуса у расчёта. У расчётов были состояния pending, paid и cancelled. Интерфейс каждый раз ставил paid. Мы построили конечный автомат и ни разу им не воспользовались, потому что в жизни расчёт записывают после того, как деньги уже ушли. Мёртвое состояние в схеме — верный признак того, что такой сущности быть не должно.

Чем они действительно отличаются

Проверку реальным использованием выдержало только одно различие: сколько людей на каждом конце.

Расход — «один ко многим». Кто-то (или несколько человек) заплатил, а стоимость делят между собой несколько человек. Самое интересное здесь — разделение.

Шапка и детализация транзакции: €186.40 заплатили вы, разделено на четверых, вам должны €139.80

Перевод — «один к одному». Деньги ушли от одного человека и пришли к другому, целиком. Делить нечего: вся сумма достаётся получателю.

Это настоящее структурное различие, и оно единственное. Поэтому видов два.

Всё, что умела выразить старая модель из четырёх сущностей, выражает и эта:

Что вы хотите записать

Как

Ужин, разделённый на пятерых

Расход

Одолжить другу €50

Перевод

Вернуть эти €50

Перевод в обратную сторону

Довести баланс до нуля

Перевод

Расчёт перестал быть особой церемонией. У него нет отдельного экрана и отдельного статуса. Это перевод, который просто доводит баланс до нуля, и он виден в истории рядом со всем остальным, потому что ничем другим он и не является. Записать его — то же самое, что записать что угодно: напишите отдал ане 15, и проверка покажет перевод — Вы отдали €15.00 кому: Anna Lopes, — и никакого разделения проверять не нужно.

Фраза «отдал анне лопес 15» разобрана как перевод: Вы отдали €15.00 кому: Anna Lopes

Правила переехали в одно место

Когда видов стало меньше, правила можно было сформулировать один раз и проверять в одном месте. Есть единственный файл, который отвечает на вопрос «корректна ли эта транзакция?», и говорит он примерно следующее:

Для расхода: суммы плательщиков, если плательщики есть, должны сложиться в итог — расход может начаться и пустым, а людей добавят позже или они сами заявят свою часть по ссылке. Участники должны свои доли.

Для перевода: ровно один плательщик, ровно один получатель, и доля получателя — вся сумма.

Вот и весь свод правил по видам. Когда сущностей было четыре, равнозначные правила были раскиданы по четырём сервисам и со временем разошлись: в ветке с несколькими плательщиками были проверки, которых не было в ветке расчётов. Теперь дверь одна, и всё проходит через неё.

Единственное исключение

В базе данных есть и третий вид, который вы никогда не создаёте сами: Упрощение.

Когда система замечает круг долгов — вы должны Marco, Marco должен Aya, а Aya — вам, — она может сократить этот круг, не меняя итога ни у кого из вас. Такие поправки должны оставаться настоящими записями, иначе балансы сдвигались бы без объяснения.

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

Стоило ли оно того?

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

Открытое разделение, распределение позиций чека, балансы в нескольких валютах, добавление людей, которые ещё не зарегистрировались, — раньше каждой из этих функций понадобилось бы четыре реализации, по одной на сущность, поэтому все они тихо откладывались. С двумя видами и общими правилами каждая оказалась небольшой задачей.

Вот довод в пользу того, чтобы сжимать модель, и он вовсе не про изящество. Каждая лишняя сущность в схеме умножает стоимость каждой функции, которую вы когда-либо добавите. Мы платили 4× за всё, чтобы сохранить различия, которые существовали только у нас в головах.


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

Читайте также