Автор: Саша Беспоясов
Сперва мы поговорим о том, что такое чистая архитектура вообще и познакомимся с такими понятиями как домен, юзкейс и слои приложения. Затем обсудим, как это применимо ко фронтенду и стоит ли вообще заморачиваться.
Далее мы спроектируем фронтенд магазина печенек по заветам чистой архитектуры. Этот магазин будет использовать React в качестве UI-фреймворка, чтобы показать, что такой подход к архитектуре применим к нему тоже. Потом с нуля реализуем один из пользовательских сценариев, чтобы понять, удобно ли это.
В коде будет немножко TypeScript, но только чтобы показать, как использовать типы и интерфейсы для описания сущностей. Всё, что мы сегодня посмотрим, можно использовать и без TypeScript, разве что код будет не таким выразительным.
Мы сегодня почти не будем говорить об ООП, поэтому пост не должен вызывать резких приступов аллергии. Мы упомянем ООП лишь один раз в конце, но спроектировать приложение нам это не помешает.
Архитектура и дизайн
Designing is fundamentally about taking things apart... in such a way that they can be put back together. ...Separating things into things that can be composed that's what design is.
— Rich Hickey. Design Composition and Performance
Системный дизайн, говорит цитата в эпиграфе, — это разделение системы таким образом, чтобы её можно было потом собрать снова. И главное — собрать легко, без лишних трудозатрат.
Я согласен: собирать домик из кубиков проще, чем разбивать большой валун на камешки. Но ещё одной из целей грамотной архитектуры я считаю расширяемость системы. Требования к программе постоянно меняются. Мы хотим, чтобы программу было легко обновлять и дорабатывать под новые требования. Чистая архитектура может помочь этой цели достичь.
Чистая архитектура
Чистая архитектура — это способ разделения ответственностей и частей функциональности по степени их близости к предметной области приложения.
Под предметной областью мы имеем в виду часть реального мира, которую моделируем программой. Это такие преобразования данных, которые отражают преобразования в реальном мире. Например, если мы обновили название товара, то замена старого имени на новое и есть доменное преобразование.
Чистую архитектуру часто называют трёхслойной, потому что приложение в ней делится слои. В оригинальном посте о The Clean Architecture приводится диаграмма с выделенными слоями:
Доменный слой
В центре — доменный слой. Это те сущности и данные, которые описывают предметную область приложения, а также код для преобразования эти данных. Домен — это ядро, которое отличает одно приложение от другого.
О домене можно думать, как о том, что точное не поменяется, если мы будем переезжать с React на Angular, или если изменим какой-то пользовательский сценарий. В случае с магазином это товары, заказы, пользователи, корзина и функции для обновления их данных.
Структура данных доменных сущностей и суть их преобразований не зависит от внешних обстоятельств. Внешние обстоятельства запускают доменные преобразования, но не определяют, как они будут протекать.
Функции добавления товара в корзину неважно, как именно товар был добавлен: самим пользователем через кнопку «Купить» или автоматически по промо-коду. Она в обоих случаях будет принимать товар и возвращать обновлённую корзину с добавленным товаром.
Прикладной слой
Вокруг домена расположен прикладной слой. В этом слое описываются юзкейсы — то есть пользовательские сценарии. Они отвечают за то, что происходит после возникновения какого-то события.
Например, сценарий «Положить товар в корзину» — это юзкейс. Он описывает действия, которые должны произойти после нажатия на кнопку. Это такой «оркестратор», который говорит:
- сейчас сходи на сервер, отправь такой запрос;
- теперь выполни такое-то доменное преобразование;
- а теперь перерисуй UI, используя новые данные.
Также в прикладном слое находятся порты — спецификации того, как наше приложение хочет, чтобы с ним общался внешний мир. Обычно порт — это интерфейс, контракт на поведение.
Порты служат «буфером» между хотелками нашего приложения и реалиями внешнего мира. Входные порты (Input Ports) говорят, как приложение хочет, чтобы к нему обращались извне. Выходные порты (Output Ports) говорят, как приложение собирается общаться с внешним миром, чтобы тот был готов к этому.
Мы рассмотрим порты и их пользу более детально позже.
Слой адаптеров
На самом внешнем слое находятся адаптеры ко внешним сервисам. Адаптеры нужны, чтобы превращать несовместимое API внешних сервисов в совместимое с хотелками нашего приложения.
Адаптеры — это отличный способ понизить зацепление между нашим кодом и кодом сторонних сервисов. Низкое зацепление уменьшает необходимость менять один модуль при изменении других.
Адаптеры часто делят на:
- управляющие (driving) — которые посылают сигналы нашему приложению;
- управляемые (driven) — которые получают сигналы от нашего приложения.
С управляющими адаптерами чаще всего взаимодействует пользователь. Например, обработка нажатия кнопки UI-фреймворком — это работа управляющего адаптера. Он работает с браузерным API (по сути сторонним сервисом) и преобразует событие в понятный нашему приложению сигнал.
Управляемые адаптеры взаимодействуют с инфраструктурой. Во фронтенде большая часть инфраструктуры — это бекенд-сервер, но иногда мы можем взаимодействовать и с какими-то другими сервисами напрямую, например, с поисковым движком.
Обратите внимание, чем дальше мы от центра — тем функциональность кода более «сервисная», тем дальше она от предметной области нашего приложения. Это будет важно позже, когда мы будем принимать решение, к какому слою отнести какой-либо модуль.
Правило зависимостей
У трёхслойной архитектуры есть правило зависимостей: только внешние слои могут зависеть от внутренних. Это значит, что:
- домен должен быть независим;
- прикладной слой может зависеть от домена;
- внешние слои могут зависеть от чего угодно.
Иногда этим правилом можно пренебречь, хотя лучше не злоупотреблять. Например, иногда бывает удобно в домене использовать какой-нибудь «полубиблиотечный» код, хотя по канонам там не должно быть никаких зависимостей. Мы рассмотрим пример такого нарушения, когда доберёмся до непосредственно кода, либо вы можете посмотреть описание репозитория, там я тоже немного об этом написал.
Беспорядочное направление зависимостей может приводить к сложному и запутанному коду. Например, нарушение правила зависимостей может приводить:
- К циклическим зависимостям, когда модуль А зависит от Б, Б — от В, а В — от А. Разруливать такие циклы сложно.
- К плохой тестируемости, когда для тестирования небольшой части приходится имитировать работу всей системы.
- К слишком высокому зацеплению, а как следствие — хрупкому взаимодействию между модулями.
Плюсы чистой архитектуры
Теперь поговорим, что нам такое разделение кода даёт. У него есть несколько преимуществ.
Обособленный домен
Вся главная функциональность приложения обособлена и собрана в одном месте — в домене. Функциональность в домене независима, а значит, её проще тестировать. Чем меньше у модуля зависимостей, тем меньше нужно инфраструктуры для тестирования, меньше нужно моков и стабов.
Также обособленный домен проще проверять на соответствие ожиданиям бизнеса. Это помогает новым разработчикам быстрее сориентироваться с тем, что приложение должно делать. Кроме того, обособленный домен помогает быстрее искать ошибки и неточности «перевода» с языка бизнеса на язык программирования.
Независимые юзкейсы
Сценарии приложения, юзкейсы описаны отдельно. Именно они диктуют, какие сторонние сервисы понадобятся. Мы подстраиваем внешний мир под свои нужды, а не наоборот — это даёт больше свободы в выборе сторонних сервисов. Например, мы можем быстро поменять платёжную систему, если нынешняя стала требовать слишком большую комиссию.
Также код юзкейсов получается плоским, тестируемым и расширяемым. Мы увидим это на примере позже.
Заменяемые сторонние сервисы
Внешние сервисы становятся заменяемыми благодаря адаптерам. Пока мы не меняем интерфейс взаимодействия с приложением, нам не важно, какой именно внешний сервис будет реализовывать этот интерфейс.
Таким образом мы создаём барьер для распространения изменений: изменения в чужом коде не влияют напрямую на наш. Адаптеры также ограничивают и распространение ошибок во время работы приложения.
Издержки чистой архитектуры
Архитектура — это в первую очередь инструмент. Как у любого инструмента у чистой архитектуры кроме выгод есть и издержки.
Требует времени
Главная издержка — это время. Оно понадобится не только на проектирование, но и на реализацию, потому что всегда проще вызвать сторонний сервис напрямую, чем писать адаптеры.
Продумывать взаимодействие всех модулей системы заранее тоже трудно, потому что мы можем не знать заранее всех требований и ограничений. При проектировании нужно держать в уме, как система может измениться, и оставлять пространство для расширения.
Иногда излишне многословна
В целом каноническое воплощение чистой архитектуры — это не всегда удобно, а иногда даже вредно. Если проект небольшой, то полная реализация будет оверхедом, который увеличит порог входа для новичков.
Может понадобиться идти на компромиссы при проектировании, чтобы не выходить за рамки бюджета или срок. Я покажу на примере, что именно подразумеваю под подобными компромиссами.
Завышает порог входа
Полная реализация чистой архитектуры может увеличить порог входа и выгнуть кривую обучения, потому что любой инструмент требует знания, как им пользоваться.
Если наоверинжинирить на старте проекта, то потом труднее будет онбордить новых разработчиков. Надо держать это в уме и следить за простотой кода.
Увеличивает количество кода
Конкретно фронтендовая проблема в том, что чистая архитектура может увеличить количество кода в финальном бандле. Чем больше кода мы отдадим браузеру, тем больше ему скачивать, парсить и интерпретировать.
За количеством кода придётся следить и принимать решения о том, где срезать углы:
- может, чуть проще описать юзкейс;
- может, обратиться к доменной функциональности прямо из адаптера, минуя юзкейс;
- может, придётся настраивать код-сплиттинг и т. д.
Как уменьшать издержки
Уменьшить количество времени и кода можно, если срезать углы и немного жертвовать каноничностью. Я вообще не фанат радикализма в подходах: если прагматичнее (выгоды > потенциальных издержек) нарушить правило, я его нарушу.
Так, можно подзабивать на некоторые аспекты чистой архитектуры до некоторого времени без видимых проблем. Минимальное необходимое же количество ресурсов, которое точно стоит уделять при проектировании — это две вещи.
Выделять домен
Именно выделенный доменный слой помогает разобраться, что мы вообще проектируем и как оно должно работать. По выделенному домену новым разработчикам проще разбираться с сутью приложения, его сущностями и отношениями между ними.
Даже если мы пропустим остальные слои и нафигачим в продакшен лапше-код, работать и рефакторить всё равно будет проще с выделенным, не размазанным по кодовой базе доменом. Другие слои можно добавлять по мере необходимости.
Соблюдать правило зависимостей
Второе правило, от которого не стоит отказываться — правило зависимостей, а точнее их направление. Внешние сервисы должны подстраиваться под нас и никогда наоборот.
Если вы чувствуете, что «дорабатываете напильником» свой код, чтобы он мог вызывать API поиска — что-то не так. Лучше напишите адаптер, пока проблема не пустила метастазы.
0 комментариев
Добавить комментарий