Как стать автором
Поиск
Написать публикацию
Обновить
СберЗдоровье
Лидеры российского медтеха

Применение статических анализаторов архитектуры на примере гексагональной архитектуры

Уровень сложностиСредний
Время на прочтение17 мин
Количество просмотров4.6K

Отсутствие четкой структурированной архитектуры проектов — не редкость в ИТ. Одни этим пренебрегают из-за маленького масштаба проекта, другие — из-за сжатых сроков разработки, третьи — из-за отсутствия экспертизы в этом вопросе. Вместе с тем, движение по этому пути — практически всегда история с «отложенными последствиями»: со временем такие проекты становится сложно поддерживать, масштабировать, администрировать и фиксить. 

Меня зовут Никита Дергачев. Я Teamlead COOL TEAM в MedTech компании СберЗдоровье. В этой статье я расскажу, почему важно структурировано выстраивать архитектуру проектов, а также покажу на примере, с помощью каких инструментов можно отслеживать соответствие архитектуры изначальным требованиям.

Архитектура проектов и ее значимость

Архитектура приложения — структура, в которой собраны все компоненты, а также описание их взаимодействия между собой, способов управления данными и пользовательским интерфейсом.

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

Так, выстраивание структурированной архитектуры проекта дает ряд преимуществ.

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

  • Упрощенная поддержка. Разработчикам проще ориентироваться в проекте, если у него регламентированная структура: так специалистам легче находить нужные модули, выявлять баги, распознавать точки роста и не только.

  • Ускорение разработки. Многие паттерны разработки и последующей эксплуатации приложений детально изучены и тщательно проработаны. Выстраивая типовую архитектуру вместо кастомной, можно легче пройти дорогу, на которой кто-то уже «набивал шишки» и применять общедоступный опыт. Соответственно, это потенциально сокращает количество ошибок и ускоряет разработку.

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

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

Для примера подробнее рассмотрим гексагональную архитектуру.

Гексагональная архитектура

Гексагональная архитектура — один из наиболее распространенных архитектурных подходов, который решает проблему разделения основной логики приложения и взаимодействия с внешними системами.

Основная цель этой архитектуры — отделить бизнес-логику приложения от сервисов, которые оно использует. Это позволяет «подключать» различные сервисы и запускать приложение без этих сервисов, а также упрощает написание автоматизированных тестов.

Гексагональная архитектура делит программное обеспечение на несколько слабосвязанных компонентов, каждый из которых соединён с другим через ряд открытых «портов». При этом в архитектуре выделяют четыре слоя.

  • Слой Domain (домена). Ядро приложения, которое содержит бизнес-логику приложения. Компонент должен быть максимально изолированным: не зависеть от инфраструктуры, фреймворка, внешних систем и библиотек.

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

  • Слой Presentation (представления). Является входной точкой приложения. Не содержит логики. Отвечает за конвертацию входных данных в DTO и передачу данных в приложение.

  • Слой Infrastructure (инфраструктуры). Отвечает за работу с файловой системой, БД, кэшем, брокерами сообщений. Выполняет задачи отправки API запросов во внешние сервисы и взаимодействия с фреймворком.

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

С портами приложения взаимодействует два типа адаптеров.

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

Вторичные (или управляемые) адаптеры, работа которых инициализируется приложением. Например, они вызываются, когда нужно организовать подключение к внешним БД.

Итого гексагональная архитектура со всем «обвесом» имеет следующий вид:

Стоит отметить, что в данной архитектуре критическое значение имеет соблюдение инверсии зависимостей — высокоуровневые модули не должны зависеть от реализации, а внутренний слой (например, домен) не должен зависеть от внешних.

Жизненный цикл работы с запросами в такой архитектуре условно можно свести к небольшому алгоритму:

  • в приложение поступает запрос по одному из адаптеров;

  • используя адаптер, запрос приходит в слой представления;

  • далее данные передаются в слой приложения;

  • затем создается сущность;

  • через инфраструктуру, используя адаптер, например, для PostgreSQL, сущность сохраняется;

  • пользователь или система получают уведомление о выполнении запроса.

Всё просто, прозрачно и прогнозируемо.

Статические анализаторы архитектуры и их значение

Статические анализаторы архитектуры — это инструменты, которые автоматически проверяют архитектуру приложения на соответствие изначально определенным требованиям. 

Использование статических анализаторов решает сразу несколько задач.

  • Ускорение code review. Проверку кода на соответствие утвержденной архитектуре можно встроить в общий пайплайн разработки, автоматизировав ее. 

  • Ускорение разработки. Анализатор использует задокументированные правила разработки, поэтому минимизирует риск создания проектов, не соответствующих ожиданиям.

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

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

Есть несколько популярных анализаторов архитектуры. Среди них:

  • Deptrac — инструмент статического анализа кода для PHP, который помогает сообщать, визуализировать и применять архитектурные решения в проектах. С его помощью можно определять архитектурные слои над классами и какие правила должны к ним применяться, а также гарантировать, что пакеты/модули/расширения в проекте действительно независимы друг от друга.

  • PHP Architecture Tester — инструмент статического анализа, предназначенный для проверки архитектурных требований. Предоставляет абстракцию естественного языка, которая позволяет определять собственные архитектурные правила и оценивать их соблюдение в коде. Позволяет контролировать соответствие зависимостей классов разрешенным пространствам имен. 

  • PHPArch — библиотека для архитектурного тестирования проектов на PHP. Позволяет выстраивать архитектурные границы в приложении и предотвращать их изменение со временем.

  • PHPUnit Application Architecture Test — расширение PHP Unit, которое предоставляет дополнительные возможности для проверки архитектуры. 

Вместе с тем, PHPArch и PHPUnit Application Architecture Test развиваются медленно или не развиваются вообще, поэтому в продовых проектах используются редко. Соответственно, алгоритм работы с анализаторами архитектуры будем разбирать на примерах Deptrac и PHP Architecture Tester.

От теории к практике: работа со статическими анализаторами архитектуры

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

Начнем с проекта.

Реализация проекта

В качестве наглядного примера реализуем интернет-магазин.

Сначала добавляем модуль с названием Shop.

Далее добавляем соответствующие слои архитектуры: Domain, Application, Presentation, Infrastructure.

В итоге получаем базовую структуру.  

Далее переходим к добавлению сущности, которой в рамках интернет-магазина будет «заказ». Для сущности добавляем два поля: «ID» и «сумма». 

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Shop\Domain\Entity;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: '`order`')]
#[ORM\HasLifecycleCallbacks]
final class Order
{
   #[ORM\Id]
   #[ORM\Column(name: 'id', type: Types::INTEGER)]
   #[ORM\GeneratedValue]
   private int $id;

   #[ORM\Column(name: 'sum', type: Types::INTEGER)]
   private int $sum;

   private function __construct()
   {
   }

   public static function create(int $sum): Order
   {
       $order = new Order();
     
       $order->sum = $sum;

       return $order;
   }
}

Примечание: Тут стоит оговориться. Исходя из правил архитектуры, домен не может зависеть от других слоев и компонентов, в том числе от фреймворка. Но, поскольку нам удобно использовать аннотации, при строгом «ручном» контроле мы можем игнорировать требования в части зависимостей.  

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

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Shop\Domain\Repository;

use Sberhealth\Demo\Shop\Domain\Entity\Order;

interface OrderRepositoryInterface
{
   public function save(Order $order): void;
}

Переходим к слою приложения. Классически здесь находится DTO и сервис или Use Case. В данном случае сервис будет принимать DTO и из него формировать сущность Order, а дальше — сохранять сущность через Repository.

Dto:

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Shop\Application\Dto;

class CreateOrderDto
{
   public function __construct(public readonly int $sum)
   {
   }
}

Сервис:

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Shop\Application\Service;

use Sberhealth\Demo\Shop\Application\Dto\CreateOrderDto;
use Sberhealth\Demo\Shop\Domain\Entity\Order;
use Sberhealth\Demo\Shop\Domain\Repository\OrderRepositoryInterface;

class OrderService
{
   public function __construct(private readonly OrderRepositoryInterface $orderRepository)
   {
   }

   public function create(CreateOrderDto $createOrderDto): void
   {
       $order = Order::create($createOrderDto->sum);

       $this->orderRepository->save($order);
   }
}

Переходим к инфраструктуре.

У нас есть интерфейс репозитория, который является портом. Нам надо создать вторичный адаптер — реализацию этого интерфейса.

В рамках примера представим, что мы работаем с PostgreSQL. 

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Shop\Infrastructure\Repository\PostgreSql;

use Sberhealth\Demo\Shop\Domain\Entity\Order;
use Sberhealth\Demo\Shop\Domain\Repository\OrderRepositoryInterface;

class PostgreSqlOrderRepository implements OrderRepositoryInterface
{
   public function save(Order $order): void
   {
       return;
   }
}

Остается реализовать слой представления.

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

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Shop\Presentation\Http\Rest;

use Sberhealth\Demo\Shop\Application\Dto\CreateOrderDto;
use Sberhealth\Demo\Shop\Application\Service\OrderService;

class OrderController
{
   public function __construct(private readonly OrderService $orderService)
   {
   }

   public function createOrder(): void
   {
       $this->orderService->create(
           new CreateOrderDto(1)
       );

       return;
   }
}

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

Теперь перейдем к тестированию статических анализаторов архитектуры. 

Работа с Deptrac

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

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

В правилах строго декларируем, что:

  • домен изолирован и ни с кем не взаимодействует;

  • слой приложения может взаимодействовать с доменом;

  • слой представления может работать со слоями приложения и домена;

  • инфраструктура может взаимодействовать с приложением и доменом.

parameters:
 paths:
   - ./src

 #Настройка слоев приложения
 layers:
   - name: Domain
     collectors:
       - type: classLike
         regex: ^Sberhealth\\Demo\\.+\\Domain\\.*

   - name: Application
     collectors:
       - type: classLike
         regex: ^Sberhealth\\Demo\\.+\\Application\\.*

   - name: Infrastructure
     collectors:
       - type: classLike
         regex: ^Sberhealth\\Demo\\.+\\Infrastructure\\.*

   - name: Presentation
     collectors:
       - type: classLike
         regex: ^Sberhealth\\Demo\\.+\\Presentation\\.*

   - name: Vendor
     collectors:
       - type: bool
         must:
           - type: classLike
             regex: ^(?!^Sberhealth\\Demo\\).*$

         # Исключение доктрины из слоя Vendor
         # для возможности использования в Domain
         must_not:
           - type: classLike
             regex:  Doctrine\\ORM\\.*

           - type: classLike
             regex: Doctrine\\DBAL\\.*

   - name: VendorDoctrine
     collectors:
       - type: classLike
         regex: Doctrine\\ORM\\.*

       - type: classLike
         regex: Doctrine\\DBAL\\.*

 # Настройка взаимодействия между слоями
 ruleset:
   Domain:
     - VendorDoctrine

   Application:
     - Domain

   Infrastructure:
     - Domain
     - Application
     - Vendor

   Presentation:
     - Application
     - Domain
     - Vendor

   Vendor:

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

Для наглядности можно рассмотреть и пример, когда проблемы есть. Например, можно в слое приложения использовать реализацию репозитория, а не контракт.

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Shop\Application\Service;

use Sberhealth\Demo\Shop\Application\Dto\CreateOrderDto;
use Sberhealth\Demo\Shop\Domain\Entity\Order;
use Sberhealth\Demo\Shop\Infrastructure\Repository\PostgreSql\PostgreSqlOrderRepository;

class OrderService
{
   public function __construct(private readonly PostgreSqlOrderRepository $orderRepository)
   {
   }

   public function create(CreateOrderDto $createOrderDto): void
   {
       $order = Order::create($createOrderDto->sum);
       $this->orderRepository->save($order);
   }
}

После запуска будем видеть следующую ошибку: 

Так же deptrac позволяет изолировать модули приложения. Допустим, у нас появится новый модуль «Delivery», отвечающий за доставку, и нам нужно, чтобы два модуля не могли взаимодействовать друг с другом. Для этого необходимо реализовать следующую настройку:

parameters:
 paths:
   - ./src

 layers:
   - name: Shop
     collectors:
       # В этом примере настроим определение
       # через директории
       - type: directory
         regex: src/Shop/.*

   - name: Delivery
     collectors:
       - type: directory
         regex: src/Delivery/.*

 ruleset:
   Shop:
   Delivery:

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

Таким образом, Deptrac — эффективный, удобный и простой инструмент, но фактически он решает только задачу проверки соблюдения правил взаимодействия между слоями. 

Работа с PHP Architecture Tester

Теперь перейдем к PHP Architecture Tester. 

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

Соответственно, всю настройку зависимостей и namespace здесь можно реализовать по принципу Deptrac. Единственное отличие — в PHP Architecture Tester используется двойное экранирование, но в целом настройки будут аналогичны тем, что мы уже реализовывали выше.

<?php

declare(strict_types=1);

namespace Sberhealth\Demo\Test\Architecture;

use PHPat\Selector\ClassNamespace;
use PHPat\Selector\Selector;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class HexagonalArchitectureTest
{
   public function testDomain(): Rule
   {
       return Phpat::rule()
           ->classes($this->getDomainClassNamespace())
           ->shouldNotDependOn()
           ->classes(
               $this->getApplicationClassNamespace(),
               $this->getInfrastructureClassNamespace(),
               $this->getPresentationClassNamespace(),
               $this->getVendorClassNamespace()
           )
           ->excluding($this->getVendorDoctrineDbalClassNamespace(), $this->getVendorDoctrineOrmClassNamespace());
   }

   public function testApplication(): Rule
   {
       return Phpat::rule()
           ->classes($this->getApplicationClassNamespace())
           ->shouldNotDependOn()
           ->classes(
               $this->getInfrastructureClassNamespace(),
               $this->getPresentationClassNamespace(),
               $this->getVendorClassNamespace()
           );
   }

   public function testInfrastructure(): Rule
   {
       return Phpat::rule()
           ->classes($this->getApplicationClassNamespace())
           ->shouldNotDependOn()
           ->classes($this->getPresentationClassNamespace());
   }

   private function getDomainClassNamespace(): ClassNamespace
   {
       return Selector::inNamespace('/^Sberhealth\\\\Demo\\\\.+\\\\Domain\\\\.*/', true);
   }

   private function getApplicationClassNamespace(): ClassNamespace
   {
       return Selector::inNamespace('/^Sberhealth\\\\Demo\\\\.+\\\\Application\\\\.*/', true);
   }

   private function getInfrastructureClassNamespace(): ClassNamespace
   {
       return Selector::inNamespace('/^Sberhealth\\\\Demo\\\\.+\\\\Infrastructure\\\\.*/', true);
   }

   private function getPresentationClassNamespace(): ClassNamespace
   {
       return Selector::inNamespace('/^Sberhealth\\\\Demo\\\\.+\\\\Presentation\\\\.*/', true);
   }

   private function getVendorClassNamespace(): ClassNamespace
   {
       return Selector::inNamespace('/^(?!Sberhealth\\\\Demo\\\\).*$/', true);
   }

   private function getVendorDoctrineOrmClassNamespace(): ClassNamespace
   {
       return Selector::inNamespace('/^Doctrine\\\\ORM\\\\.*/', true);
   }

   private function getVendorDoctrineDbalClassNamespace(): ClassNamespace
   {
       return Selector::inNamespace('/^Doctrine\\\\DBAL\\\\.*/', true);
   }
}

Для подключения дополнительных проверок в phpstan, необходимо указать класс в файле phpstan.neon

includes:
   - vendor/phpat/phpat/extension.neon

services:
   -
       class: Sberhealth\Demo\Test\Architecture\HexagonalArchitectureTest
       tags:
           - phpat.test

Если всё сделано правильно, при запуске теста через PHPStan с зашитыми проверками PHP Architecture Tester ошибок по архитектуре не будет. 

Теперь можно убедиться в корректности настроек и нарушить правило. Также в сервисе попробуем использовать реализацию репозитория.

В результате получаем упавший тест, поскольку приложение не может зависеть от инфраструктуры.

Далее можем перейти к рассмотрению дополнительных возможностей инструмента. Например, с его помощью можно проверить соответствие названия интерфейса стандарту PSR — они должны заканчиваться словом «interface». 

Реализуем соответствующую проверку. Для этого определяем все интерфейсы и через регулярные выражения задаем упомянутые критерии для их названия. 

public function testInterfaceNaming(): Rule
{
   return Phpat::rule()
       ->classes(Selector::isInterface())
       ->shouldBeNamed('/.+Interface/', true);
}

Так же можно реализовать проверку на то, все ли сущности проекта являются «final».

public function testEntityFinal(): Rule
{
   return Phpat::rule()
       ->classes($this->getEntityClassNamespace())
       ->shouldBeFinal();
}

private function getEntityClassNamespace(): ClassNamespace
{
   return Selector::inNamespace('/^Sberhealth\\\\Demo\\\\.+\\\\Domain\\\\Entity.*/', true);
}

Помимо этого, с помощью PHP Architecture Tester можно проводить еще целый ряд проверок для разных сценариев и вводных данных. 

Таким образом, PHP Architecture Tester — максимально гибкий и универсальный инструмент, который позволяет организовывать проверки разной сложности и направленности, а также практически автоматизировать тесты на соответствие кода и архитектуры любым внутренним и внешним стандартам компании.

Что в итоге

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

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

Теги:
Хабы:
Всего голосов 7: ↑7 и ↓0+9
Комментарии6

Публикации

Информация

Сайт
sberhealth.ru
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия
Представитель
Чапля Катя