Андрей Копылов, наш технический директор, рассказывает, какой подход к проектированию архитектуры приложений использует команда веб-разработчиков AREALIDEA, и, чем KISS Architecture, его собственная разработка, так хороша.
Существует масса подходов к проектированию архитектуры приложения. MVC, DDD, Clean Architecture и множество других.
MVC хорошо подходит для маленьких приложений. При попытке масштабирования MVC превращается в самую распространенную архитектуру в мире IT — Большой ком грязи.
DDD отличная архитектура, но ее никто не понимает. Разве что сам создатель и пара архитекторов. Цель же архитектуры в том, чтобы она была понятна каждому разработчику.
Clean Architecture отличная архитектура, но ее полная реализация имеет смысл для огромных приложений. Для малых и средних мне она показалась слишком сложной.
Современные тенденции — переход к сервисам и микросервисам — на этом фоне Clean Architecture становится чересчур тяжеловесной.
Казалось бы тогда давайте возьмем MVC для микросервиса и на этом и остановимся. Но нет, такой велосипед нам не подходит.
Составляющие
Велосипед для проектов в нашем агентстве собран из запчастей от разных архитектурных подходов.
Вот компоненты, которые нужны для создания понятной и удобной структуры:
- Routers
- Controllers
- Views
- Services
- Models
Слои
Router
Router отвечает за маршрутизацию запросов. Размер роутера и их количество косвенно говорит о размере вашего приложения. Для большого монолитного приложения может быть более одного слоя роутеров.
Router присутствует в любой архитектуре, но часто в неявном виде. А поскольку явное лучше неявного, то стоит его вытащить наружу — сделать составной частью архитектуры.
Controller
Контроллер является прослойкой между роутером и сервисами. Бизнес логики в контроллере быть не должно.
Каждый контроллер управляет только одной сущностью. Если нужно больше сущностей, то нужно добавить еще один контроллер.
Количество и размер контроллеров косвенно говорит о размере вашего приложения. Вертикальный слой под контроллером можно выделить в отдельный микросервис.
Views
View находится в одном слое с контроллером, отвечает за конечное отображение данных. Контроллер после получения данных из сервиса передает данные во View и возвращает View для отображения.
В предельном случае View представляет собой JSON, XML и подобные форматы.
Services
Только Сервис может содержать бизнес логику. Сервис обычно обращается только к одной модели. Сервис может вызвать другой сервис.
Слой сервисов разделяем на Commands и Queries (команды и запросы). Это стандартный подход для CQRS.
Один сервис выполняет только одну функцию. Приватных функций может быть сколько угодно, а публичная только одна. Название сервиса начинается с глагола. Примеры: GetUsers, GetPostById, UpdateUser, PublishPost. Именно название сервиса намекает на правильное разделение функционала.
В Queries помещаем сервисы, которые не изменяют базу данных. Query содержит одну публичную функцию get. В Commands помещаем сервисы, которые изменяют БД. Command содержит одну публичную функцию execute.
Models
Модель содержит только простейшую логику, связанную с чтением и сохранением данных. Причем эти манипуляции могут быть и не связаны с БД.
Если модель работает с БД, то одна модель обслуживает только одну или несколько таблиц.
Рецепты
Микросервис
Микросервис в моем понимании должен управлять только одной сущностью. Поэтому архитектура для простейшего микросервиса будет выглядеть таким образом:
- один Роутер;
- один Контроллер;
- несколько Views;
- несколько Сервисов;
- одна Модель.
Сервис
Сервис — это мини приложение. Оно содержит:
- один Роутер;
- несколько Контроллеров;
- несколько Views;
- несколько Сервисов;
- несколько Моделей.
Монолит
Монолит — это большое приложение. Монолиты никто не любит по причине их монструозности. Монолит оправдан, если следовать подходу Monolith first. В таком состоянии ваше приложение может пребывать довольно долго.
Монолит содержит в себе:
- один СуперРоутер;
- несколько обычных Роутеров;
- много Контроллеров;
- много Views, много Сервисов;
- много Моделей.
Это начинает выглядеть немного страшно. Здесь явно видно дополнительное вертикальное разделение слоев. Это позволяет оставаться приложению пока еще управляемым и поддерживаемым. Распиливание монолита на части становится чисто механической задачей.
Для сохранения стройности архитектуры нужно:
- Добавить один верхнеуровневый роутер, который будет разруливать глобальные пути — СуперРоутер.
- Распределить файлы в структуре помодульно. То есть в соответствии с будущим распилом на отдельные сервисы.
Тестирование
В рамках рассматриваемой архитектуры тщательному тестированию подлежат только сервисы — только в них заложена бизнес логика. А мокать нужно только Модели.
Если у вас возникает желание протестировать, что-то кроме сервисов, то вероятно место для логики выбрано неправильно.
Заключение
На мой взгляд, KISS Architecture подходит для 80% проектов и обеспечивает плавную эволюцию проекта.
Этот архитектурный подход будет понятен всем разработчикам и для его применения на практике не нужно читать толстенные книги про DDD.
Комментарии (25)
VolCh
19.02.2019 11:03+1> Каждый контроллер управляет только одной сущностью. Если нужно больше сущностей, то нужно добавить еще один контроллер.
Вот реально так? На примере абстрактного интернет-магазина: есть сущность «заказ», есть сущность «адрес доставки», есть сущность «строка заказа» (товар, количество) — и как минимум последняя не имеет никакого смысла вне первой. Зачем для неё отдельный контроллер?aak74
19.02.2019 11:45Вот реально так? На примере абстрактного интернет-магазина: есть сущность «заказ», есть сущность «адрес доставки», есть сущность «строка заказа» (товар, количество) — и как минимум последняя не имеет никакого смысла вне первой. Зачем для неё отдельный контроллер?
Тут тоже наверное не вполне адекватно сформулировал.
Отдельный контроллер будет для сущности Заказ. И отдельный для сущности Товар.
Дальше дробить пожалй нет смысла.VolCh
19.02.2019 14:18В толстой книжке по DDD такие сущности называются агрегатами. Только ради общепринятого именования их стоит читать.
webdevium
19.02.2019 12:36Лично я пишу бизнес логику в сервисах уже несколько лет. Но мне кажется, что такое чрезмерное разделение на один «invoke» — как то через чур. Скажите, какая причина такому делению?
aak74
19.02.2019 12:39При большом количестве разработчиков (больше 2-х) и длинных релизах очень много конфликторв было.
Сейчас конфликтов практически нет.ThunderCat
19.02.2019 15:36+1«У нас было много разработчиков и мало файлов, мы сделали много файлов и теперь все ок»
Вам не кажется что где-то что-то пошло не так на этапе распределения задач?aak74
19.02.2019 16:571. Не всегда возможно распределить задачи так как хочется. Нам приходится сталкиваться с суровой действительностью, в которой Заказчик может диктовать собственные правила.
2. В Clean Architecture есть Use Case, который как раз отвечает только за одну фунцию. По крайней мере именно так я понял.Guitariz
19.02.2019 17:171. Вам заказчик диктует, кто будет зададчку решать? Интересный феодализм
2. Скорее там речь о типе задач. И гарантируется он не просто «один класс — одна функция», а круговой моделью взаимодействия. Данная модель гарантирует, что презентер не стучитсяв модель или воркер например.aak74
19.02.2019 17:22Заказчик дикутет количество задач, которые должны выполняться одновременно. А еще он диктует в какой последовательности и когда будут сдаваться те или иные фичи.
Бооооль.
VolCh
19.02.2019 17:41Немного странно. По идее количество конфликтов в ситуации «много классов(файлов) с одним методом» и «один класс(файл) с много методов» должно быть лишь немного меньше, если по методам аналогично функциональность распределена. Или это фикс проблемы из разряда «давайте перейдём на микросервисы, чтобы дергали только фасад модуля, а не его напрямую»
kurliker
19.02.2019 14:56+1Router=> Controller=> Service=> DAL
It does not matter how you call it, it is just old good architecture
powerman
19.02.2019 18:57Пока отдельный компонент (напр. микросервис) достаточно мал и прост (условно говоря, если полезного кода в микросервисе до 500 строк — за вычетом стандартно-инфраструктурного кода вроде сетевого I/O, сериализации, логов/метрик), то ему вообще никакая явная архитектура не требуется. Может иметь смысл структурировать такие компоненты любым общепринятым в компании способом, просто чтобы быстрее находить нужный код в любом компоненте, но не более того, и чем меньше формальных требований предъявляет такая структура — тем лучше. Описанное в статье ближе всего именно к такому способу структурирования, хотя и избыточно раздутому, на мой взгляд.
А вот как только сложность этого компонента вырастает до состояния, когда отсутствие явной архитектуры начинает вызывать дискомфорт, пусть даже и минимальный — в этот момент стоит перевести этот компонент на Clean. Clean может казаться избыточной, но создание нескольких дополнительных интерфейсов и пара лишних копирований всех данных между почти идентичными структурами — небольшая цена за те возможности, которые мы получаем. Эта "избыточность" Clean в большей степени психологическая проблема, а не техническая — рефлекторно хочется избегать лишнего копирования из соображений производительности и написания однотипного кода, потому что обычно он создаёт неприятные проблемы — но в случае Clean этого не происходит.
А вообще всё это мелочи, вопросы архитектуры внутри компонентов в наши дни проработаны достаточно хорошо. Сложная часть в архитектуре начинается там, где нужно разделить весь проект на вышеупомянутые компоненты, чтобы большинство из них было достаточно небольшими, и чтобы связи между ними при этом оставались ясными и эффективными. И вот эта задача в статье вообще не упоминается. И большой комок грязи возникает не только из MVC, он вполне может получиться и из Clean, если не разделить большой проект на компоненты — просто в случае Clean это будет несколько больших комков грязи, тщательно разделённых интерфейсами. :)
Всё вышеописанное касается бэкенда. Для UI зачастую MVC/MVVM подойдёт лучше.
Guitariz
Меня одного здесь что-то смущает?
И как View и Controller оказались в одном слое, если даже задачи у них принципиально разные.
И да, Clean architecture здесь вообще не при чем, нет в ней никаких царь-роутеров и царь-сервисов
aak74
Задачи разные да.
Они находятся в одном слое с точки зрения отдаления от пользователя.
Guitariz
1) архитектура кода не строится исходя из точки зрения отдаления от пользователя
2) пользователь view может увидеть, а контроллеры — нет.
3) слой — состоящий из принципиально разных элементов — или не слой, или дилетантская ошибка.
Буду честен — статья о классическом представлении Layered pattern, как проектировали приложения десятилетиями. Какую-нибудь толстую книгу о проектировании все-таки стоит прочесть, иначе вы правда не поймете, почему сейчас архитектуру, подобную вашей, пытаются избегать.
remzalp
Ок, а какие на данный момент best practices?
Guitariz
Сложный вопрос. По мобилкам это MVVM и Clean (только настоящий, здесь никакого Clean Architecture нет)
Вопрос не в best practice, Layered pattern с монолитом обладает кучей недостатков, с которой в разной степени успешности борются другие архитектуры.
Выражение «KISS Architecture подходит для 80% проектов и обеспечивает плавную эволюцию проекта» подходит для любой архитектуры, и даже для ее отсутствия — все будет от пряморукости разработчика зависеть)
aak74
Все так. Тег выбран неправильно.
Этот «архитектурный» подход ближе всего к MVC с введением слоя Сервисов. Который в свою очередь вертикально разделяется на Query и Command.
Ну и фишка этого подхода в том, что Query или Command выполняет только одну задачу. Это позволяет не иметь конфликтов при разработке.
Собственно вся суть подхода описана в этом комменте.
Guitariz
Не уверен, что это поможет — при мерже, проблем, несомненно, будет меньше, а вот размазывание кода на много мелких сущностей с одним методом — хорошая заявка на «забыл внестив изменения во все необходимые классы»
aak74
Нельзя исключать такой возможности. Но в нашем случае результаты пока положительные.
Guitariz
Вообще не понятно, что здесь взято не из MVC.
Clean — не сложный, он то как раз гораздо понятнее и популярнее, чем описаные выше правила.
VolCh
По хорошему, в MVC как раз подразумевается, что модель — это набор методов, фасад, прячущий за собой бинес-логику, инфраструктуру и т. п., а не какие-то объекты типа ActiveRecord, которіми контроллер оперирует.
aak74
Наверное да. Но на практике я в модели часто видел работу с БД. Да и сам так делал.
VolCh
Работа с БД должна быть в модели в MVC (не путать с моделью DDD), но факт наличия БД в ней не должен протекать в контроллер и вью.
VolCh
MVC — это паттерн UI слоя :)