Андрей Копылов, наш технический директор, рассказывает, какой подход к проектированию архитектуры приложений использует команда веб-разработчиков 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, много Сервисов;
  • много Моделей.


Это начинает выглядеть немного страшно. Здесь явно видно дополнительное вертикальное разделение слоев. Это позволяет оставаться приложению пока еще управляемым и поддерживаемым. Распиливание монолита на части становится чисто механической задачей.


Для сохранения стройности архитектуры нужно:


  1. Добавить один верхнеуровневый роутер, который будет разруливать глобальные пути — СуперРоутер.
  2. Распределить файлы в структуре помодульно. То есть в соответствии с будущим распилом на отдельные сервисы.

Тестирование


В рамках рассматриваемой архитектуры тщательному тестированию подлежат только сервисы — только в них заложена бизнес логика. А мокать нужно только Модели.


Если у вас возникает желание протестировать, что-то кроме сервисов, то вероятно место для логики выбрано неправильно.


Заключение


На мой взгляд, KISS Architecture подходит для 80% проектов и обеспечивает плавную эволюцию проекта.


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

Комментарии (25)


  1. Guitariz
    19.02.2019 09:55

    Сервис

    Сервис — это мини приложение. Оно содержит:

    один Роутер;
    несколько Контроллеров;
    несколько Views;
    несколько Сервисов;
    несколько Моделей.


    Меня одного здесь что-то смущает?
    И как View и Controller оказались в одном слое, если даже задачи у них принципиально разные.
    И да, Clean architecture здесь вообще не при чем, нет в ней никаких царь-роутеров и царь-сервисов


    1. aak74
      19.02.2019 10:28

      И как View и Controller оказались в одном слое, если даже задачи у них принципиально разные.

      Задачи разные да.
      Они находятся в одном слое с точки зрения отдаления от пользователя.


      1. Guitariz
        19.02.2019 10:37

        1) архитектура кода не строится исходя из точки зрения отдаления от пользователя
        2) пользователь view может увидеть, а контроллеры — нет.
        3) слой — состоящий из принципиально разных элементов — или не слой, или дилетантская ошибка.
        Буду честен — статья о классическом представлении Layered pattern, как проектировали приложения десятилетиями. Какую-нибудь толстую книгу о проектировании все-таки стоит прочесть, иначе вы правда не поймете, почему сейчас архитектуру, подобную вашей, пытаются избегать.


        1. remzalp
          19.02.2019 11:17

          Ок, а какие на данный момент best practices?


          1. Guitariz
            19.02.2019 11:23

            Сложный вопрос. По мобилкам это MVVM и Clean (только настоящий, здесь никакого Clean Architecture нет)
            Вопрос не в best practice, Layered pattern с монолитом обладает кучей недостатков, с которой в разной степени успешности борются другие архитектуры.
            Выражение «KISS Architecture подходит для 80% проектов и обеспечивает плавную эволюцию проекта» подходит для любой архитектуры, и даже для ее отсутствия — все будет от пряморукости разработчика зависеть)


            1. aak74
              19.02.2019 11:49
              +1

              здесь никакого Clean Architecture нет

              Все так. Тег выбран неправильно.
              Этот «архитектурный» подход ближе всего к MVC с введением слоя Сервисов. Который в свою очередь вертикально разделяется на Query и Command.
              Ну и фишка этого подхода в том, что Query или Command выполняет только одну задачу. Это позволяет не иметь конфликтов при разработке.

              Собственно вся суть подхода описана в этом комменте.


              1. Guitariz
                19.02.2019 12:02

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


                1. aak74
                  19.02.2019 12:07

                  Нельзя исключать такой возможности. Но в нашем случае результаты пока положительные.


                  1. Guitariz
                    19.02.2019 12:16

                    Вообще не понятно, что здесь взято не из MVC.
                    Clean — не сложный, он то как раз гораздо понятнее и популярнее, чем описаные выше правила.


              1. VolCh
                19.02.2019 14:40

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


                1. aak74
                  19.02.2019 15:15

                  Наверное да. Но на практике я в модели часто видел работу с БД. Да и сам так делал.


                  1. VolCh
                    19.02.2019 15:24
                    +1

                    Работа с БД должна быть в модели в MVC (не путать с моделью DDD), но факт наличия БД в ней не должен протекать в контроллер и вью.


    1. VolCh
      19.02.2019 10:57
      +1

      MVC — это паттерн UI слоя :)


  1. VolCh
    19.02.2019 11:03
    +1

    > Каждый контроллер управляет только одной сущностью. Если нужно больше сущностей, то нужно добавить еще один контроллер.

    Вот реально так? На примере абстрактного интернет-магазина: есть сущность «заказ», есть сущность «адрес доставки», есть сущность «строка заказа» (товар, количество) — и как минимум последняя не имеет никакого смысла вне первой. Зачем для неё отдельный контроллер?


    1. aak74
      19.02.2019 11:45

      Вот реально так? На примере абстрактного интернет-магазина: есть сущность «заказ», есть сущность «адрес доставки», есть сущность «строка заказа» (товар, количество) — и как минимум последняя не имеет никакого смысла вне первой. Зачем для неё отдельный контроллер?

      Тут тоже наверное не вполне адекватно сформулировал.
      Отдельный контроллер будет для сущности Заказ. И отдельный для сущности Товар.
      Дальше дробить пожалй нет смысла.


      1. VolCh
        19.02.2019 14:18

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


  1. webdevium
    19.02.2019 12:36

    Лично я пишу бизнес логику в сервисах уже несколько лет. Но мне кажется, что такое чрезмерное разделение на один «invoke» — как то через чур. Скажите, какая причина такому делению?


    1. aak74
      19.02.2019 12:39

      При большом количестве разработчиков (больше 2-х) и длинных релизах очень много конфликторв было.
      Сейчас конфликтов практически нет.


      1. ThunderCat
        19.02.2019 15:36
        +1

        «У нас было много разработчиков и мало файлов, мы сделали много файлов и теперь все ок»
        Вам не кажется что где-то что-то пошло не так на этапе распределения задач?


        1. aak74
          19.02.2019 16:57

          1. Не всегда возможно распределить задачи так как хочется. Нам приходится сталкиваться с суровой действительностью, в которой Заказчик может диктовать собственные правила.
          2. В Clean Architecture есть Use Case, который как раз отвечает только за одну фунцию. По крайней мере именно так я понял.


          1. Guitariz
            19.02.2019 17:17

            1. Вам заказчик диктует, кто будет зададчку решать? Интересный феодализм
            2. Скорее там речь о типе задач. И гарантируется он не просто «один класс — одна функция», а круговой моделью взаимодействия. Данная модель гарантирует, что презентер не стучитсяв модель или воркер например.


            1. aak74
              19.02.2019 17:22

              Заказчик дикутет количество задач, которые должны выполняться одновременно. А еще он диктует в какой последовательности и когда будут сдаваться те или иные фичи.
              Бооооль.


      1. VolCh
        19.02.2019 17:41

        Немного странно. По идее количество конфликтов в ситуации «много классов(файлов) с одним методом» и «один класс(файл) с много методов» должно быть лишь немного меньше, если по методам аналогично функциональность распределена. Или это фикс проблемы из разряда «давайте перейдём на микросервисы, чтобы дергали только фасад модуля, а не его напрямую»


  1. kurliker
    19.02.2019 14:56
    +1

    Router=> Controller=> Service=> DAL
    It does not matter how you call it, it is just old good architecture


  1. powerman
    19.02.2019 18:57

    Пока отдельный компонент (напр. микросервис) достаточно мал и прост (условно говоря, если полезного кода в микросервисе до 500 строк — за вычетом стандартно-инфраструктурного кода вроде сетевого I/O, сериализации, логов/метрик), то ему вообще никакая явная архитектура не требуется. Может иметь смысл структурировать такие компоненты любым общепринятым в компании способом, просто чтобы быстрее находить нужный код в любом компоненте, но не более того, и чем меньше формальных требований предъявляет такая структура — тем лучше. Описанное в статье ближе всего именно к такому способу структурирования, хотя и избыточно раздутому, на мой взгляд.


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


    А вообще всё это мелочи, вопросы архитектуры внутри компонентов в наши дни проработаны достаточно хорошо. Сложная часть в архитектуре начинается там, где нужно разделить весь проект на вышеупомянутые компоненты, чтобы большинство из них было достаточно небольшими, и чтобы связи между ними при этом оставались ясными и эффективными. И вот эта задача в статье вообще не упоминается. И большой комок грязи возникает не только из MVC, он вполне может получиться и из Clean, если не разделить большой проект на компоненты — просто в случае Clean это будет несколько больших комков грязи, тщательно разделённых интерфейсами. :)


    Всё вышеописанное касается бэкенда. Для UI зачастую MVC/MVVM подойдёт лучше.