Предпосылки: почему мы решили использовать чистую архитектуру
- Оценка задач в команде была завязана только на тимлиде — он решал, сколько времени примерно уйдет на задачу. Оттого оценки часто не совпадали с реальностью. В них просто не закладывали время на проектирование, ревью, риски. А мы были уверены, что чистая архитектура могла нам в этом помочь.
- Мы хотели писать в код, который бы максимально отражал бизнес-правила — то есть чтобы он не был оторван от реальности и чтобы задачи описывались сценариями использования. Особенно это было актуально, потому что мы следовали подходам «Документация как код» и «Инфраструктура как код».
- Поскольку мы имели дело с монолитом, все элементы системы были прочно связаны друг с другом. И если мы хотели изменить какую-то часть сервиса, мы ловили баги там, где не ожидали. Чистая архитектура должна была ослабить связь между объектами в хранилище.
Что это вообще такое — чистая архитектура
Как, по сути, работает чистая архитектура
При чем здесь бизнес
- Проще говоря: бизнес не поймет техническую терминологию из бизнес-требований, зато сможет провалидировать свои пожелания относительно будущего сервиса. Бизнес-правило: отказоустойчивость при заключении правительственных контрактов. Бизнес-требование: использование дата-центров, использование конкретных СУБД и т. п.
Модели предметной области
- В богатой модели предметной области слой служб очень тонок, а иногда вообще отсутствует. Вся бизнес-логика заключена в сущностях (те самые «заказ» или «покупатель»), а те реализованы в виде методов внутри классов. В богатой модели сущности способны самостоятельно обеспечивать свои инварианты, что делает такую модель полноценной с точки зрения объектно-ориентированного подхода.
- Посмотрим пример. У нас была доменная сущность X, которая в разных частях системы могла состоять из разных полей и вести себя по-разному. При богатой модели никакой из методов не смог бы полноценно наполнить ее, не затрагивая другие сущности. Если бы нам пришлось менять бизнес-правила, мы бы меняли все сущности, с которыми взаимодействует X. А мы этого не хотели.
Паттерны, которые мы использовали
- Мы писали на Python и использовали библиотеку Pydantic. Но можно пользоваться любой. Кроме разве что attrs, в которой наследование сущностей работает криво. В итоге, мы могли использовать некие миксин-классы для сущностей. Предположим, в какой-то модели есть сущность «Клиент» и есть ее принадлежность «Пользователь клиента». В этом случае мы можем использовать сущность «Клиент» как миксин для сущности «Пользователь». Далее мы получаем требования к нашим сущностям в виде поддержки сериализации JSON, чтобы было удобно передавать из слоя в слой, в том числе в слои представления, и отдавать в пользовательский интерфейс.
Unit of work
Data mapper
- слой, отвечающий за условия валидации и фильтрации;
- слой управления данными, о которых мы поговорим далее;
- слой, который организовывал доступ к хранилищу и выполнял роль некоего транслятора команд под конкретный внешний источник данных.
Репозиторий
- Например, работа с фреймворком. Так как у нас работа с фреймворком происходила в маппере, репозиторий позволял нам скрыть это и, при ослаблении связи этих слоев, менять мапперы на лету. До этого мы использовался Active Record и ORM для фильтрации объектов, а теперь начали обращаться к методу репозитория, чтобы получить некий набор сущностей. Маппер же у нас теперь обеспечивал лишь перевод искомых сущностей на язык ORM, SQL и т. п. При этом условия фильтрации мы тоже старались выносить из репозиториев в спецификации.
Спецификация
- На полях замечу, что не стоит с этими условиями сильно упарываться. Есть условия, которые в принципе нет смысла класть в спецификацию. Всё-таки спецификация должна содержать какие-то действительно сложные условия, а не примитивные и интуитивно понятные.
Как связаны Use-кейсы + Unit of work + сервисы
- Например. У нас может быть клиент и его пользователь. Предположим, пользователь хочет поменять аватарку и имя. При этом у сущности клиента есть доступ к некоему реестру сотрудников — там мы тоже должны внести изменения. Значит, имеет смысл поделить логику: изменение аватара — вывести в так называемый инфраструктурный сервис, изменение в реестре — вывести в некий доменный сервис, а затем написать метод для репозитория пользователя, в котором будут изменения его данных. При этом нужно объединить всё это конкретным сценарием.
Энтропия и когнитивная нагрузка
- Теперь разработчик должен был продумать дизайн задачи еще до ее реализации. Это позволяло уменьшать трудозатраты: многие нестыковки в требованиях мы выявляли еще до того, как переходили к написанию кода. Параллельно начали вести реестр архитектурных решений, в котором в том числе фиксировали решение некоторых типовых задач. Всё это позволило нам вести историю изменения архитектуры, а аналитики и проджекты могли в любой момент обратиться к решениям команды, которые были описаны, обоснованы, внедрены и протестированы.
Теперь видно ошибки в системе и появилась возможность лучше расследовать инциденты
Приятные результаты
- До введения описанного набора паттернов и и вообще до чистой архитектуры разработчик мог, например, не запустить фронт и не понимать, как его изменения повлияют на бизнес. После мы получили готовый чек-лист того, как правильно приступать к задаче. В нем были названы четкие шаги и приведен список артефактов, которые нужно было получить по итогу.
- Стало легче оценивать задачи и производить валидацию оценок. Я был лидом проекта, и мне стало проще отслеживать, что происходит в задачах, куда были добавлены изменения и какие это были изменения.
- Мне было проще понять, какой Use-кейс можно переиспользовать в другой части системы, потому что я стал лучше ее понимать. А понимал я ее лучше, потому что у меня не было необходимости погружаться глубоко в код. А разработчикам было проще, потому что они теперь ориентировались на Use-кейсы. Фактически у нас появилось коллективное владение кодом.
Кому подойдет чистая архитектура
Комментарии и обсуждения статьи на habr.