Сферические функции в вакууме

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

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

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


На что способен пользователь?

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

Из общей формулировки Клиент в определенном Состоянии способен выполнить определенное Действие родилась идея о том, что пользователи обладают определенными Способностями, и набор этих Способностей со временем меняется в зависимости от состояния пользователя. Чем-то это похоже на ролевые полномочия (role permissions) в моделях разграничения доступа.

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


Дерево Категорий пользователей

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

Идея далеко не нова, в 1970-х Эдгар Дийкстра предлагал фиксировать конечное множество всех возможных состояний системы и нумеровать их. Но мы, как пресловутые миллениалы, изобрели это самостоятельно :)

Так вот, мы определили, какие же состояния важны для пользователя, или другими словами, как сгруппирована доступная функциональность продукта. Оказалось, (1) что их совсем немного, (2) что они имеют бизнес-смысл и (3) что некоторые из них “расширяют” другие. Поэтому нам показалось логичным представить этот набор состояний в виде дерева, в корне которого находилась абстрактная категория “Пользователь”, а узлами стали все возможные состояния, при этом “расширяющие” состояния расположены на один уровень дальше от корня, чем “расширяемые”.

Состояния мы назвали Категориями Пользователей, к каждой Категории мы привязали набор определенных ранее Способностей. Категории, очевидно, были взаимоисключающими, то есть пользователь не мог попасть в две категории одновременно. Категория конкретного пользователя во время выполнения программы определялась динамически, мы двигались от корня дерева категорий и подходом “breadth-first” проверяли выполнение условий для попадания в одну из дочерних категорий. Если попали в какую-то категорию - проверяли выполнение условий для всех дочерних для нее узлов. В какой-то момент дальнейший расчет становился невозможным, и текущая на этот момент категория и являлась категорией пользователя.

С этого момента проверки доступности абсолютно всех действий пользователя превратились в единообразный механизм: определяем Категорию пользователя и смотрим, доступна ли этой Категории запрошенная Способность.


Результат

В ADR, посвященной этому решению, мы зафиксировали следующие последствия:

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

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

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

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