В данной статье мы коротко пройдемся по теории и на практике разберемся как перевести любое Legacy приложение на гексагональную архитектуру. Повествование будет в контексте фреймворка Symfony и PHP 7.4, но синтаксис приведенных примеров настолько прост что вы без труда поймете как сделать так же на вашем языке программирования (если он поддерживает ООП).
За свою карьеру я работал над многими проектами Symfony, и одна из самых частых проблем, с которыми клиенты звонят в нашу компанию, заключается в том, что их программное обеспечение «заблокировано» старой версией фреймворка или оно стало необслуживаемым, потому что поиск и исправление ошибок обходится слишком дорого.
Обычно, я стараюсь хорошо разобраться, почему эти устаревшие проекты находятся в таком состоянии. И часто я обнаруживал общую закономерность: команде в начале проекта нужно быстро создать приложение с нуля, потому что строгие сроки поджимают.
Процесс разработки у них начинается примерно так:
Такие шаги для меня не лучшая практика, все-же сначала лучше разобраться в предметной области и её характеристиках вместо того, чтобы немедленно начинать что-то разрабатывать.
Я думаю, что в описанной ранее ситуации они руководствовались типичным flow разработки на фреймворке.
На мой взгляд, лучше сосредоточить свои усилия на предметной области, и вам нужно рассматривать Symfony (или фреймворк в целом) как инструмент, а не основное ядро программного обеспечения, потому что реальная ценность вашего программного обеспечения — это «domain», логика которую вы разработаете для решения проблем.
Ориентация на фреймворк имеет множество побочных эффектов, и одним из самых опасных является соединение домена и фреймворка, которое может создать множество проблем, например:
Но не пугайтесь, существует архитектура, которая поможет вам избежать этих проблем: гексагональная архитектура.
Гексагональная архитектура была изобретена Алистером Коберном в попытке избежать известных структурных ошибок в объектно-ориентированном проектировании программного обеспечения, таких как нежелательные зависимости между уровнями и загрязнение кода пользовательского интерфейса бизнес-логикой, и опубликована в 2005 году.
Гексагональная архитектура делит систему на несколько слабо связанных взаимозаменяемых компонентов, таких как ядро приложения, база данных, пользовательский интерфейс, тестовые сценарии и интерфейсы с другими системами. Такой подход является альтернативой традиционной многослойной архитектуре. (Википедия)
Когда я много раз читаю и объясняю это определение, разработчики спрашивают меня: не приведет ли это к оверинжинирингу проекта?
Что ж, у вас есть больше классов, больше концепций и больше моментов, когда вам нужно много подумать о правильном расположении класса, названии класса или лучшем имени для переменной. Однако, это зависит только от вас, я могу лишь рекомендовать попробовать применить эту стратегию и улучшить свои навыки с ее помощью.
Проект, написанный 10 лет назад, заблокирован старой версией PHP, и вы хотите перейти на новую версию.
Обновление PHP означает, что вам необходимо обновить структуру и composer-пакеты, затрагивающие бизнес-логику, потому что все взаимосвязано.
Вы не можете выполнить безопасное обновление, потому что код не полностью покрыт тестами.
В этом случае ваше приложение — необслуживаемое (not maintainable).
Все эти проблемы являются типичными для проектов где домен и фреймворк связаны.
В гексагональной архитектуре вы можете разделить фреймворк и домен, чтобы вы могли обновлять фреймворк и сторонние пакеты, затрагивая лишь небольшую конкретную часть вашего кода, а не бизнес-логику.
Чтобы разделить фреймворк и домен, на практике я имею в виду разделение их по разным директориям. Я поясню это через минуту.
Еще один хороший пример «связанного кода» — это когда у вас есть внешние сервисы, и они могут на что-то влиять в вашем приложении.
Предположим, у вас есть поставщик платежных шлюзов, который выпускает новую версию, а ваша текущая версия, используемая в вашем приложении, уже перестала поддерживаться.
Можете переключиться на новую версию или заменить ее другим поставщиком шлюза, но вы знаете, что вам придется переписать множество мест по всему проекту, потому что ваш домен сильно связан с библиотекой или службой.
Таким образом, вам нужно приложить много усилий, чтобы переписать множество мест в коде, где вдобавок вы можете допустить ошибки.
В гексагональной архитектуре вы можете изменять/менять только адаптеры, не затрагивая логику домена, поскольку она отделена от фреймворка.
Пример связанного кода:
class Payment
{
public function pay(Request $request): void
{
$gateway = new YourBankGateway();
$gateway->pay($request->get('amount'));
}
}
На мой взгляд, в этом классе есть несколько проблем:
Попробуем расцепить этот код:
interface GatewayProvider {
public function pay(Money $amount): void;
}
class YourBankGateway implements GatewayProvider {
public function pay(Money $amount): void
{
//do stuff..
}
}
class Payment {
private GatewayProvider $gateway;
public function __construct(GatewayProvider $gateway)
{
$this->gateway = $gateway;
}
public function payThroughGateway(Money $amount): void
{
$this->gateway->pay($amount);
}
}
В этом и многих других случаях использование интерфейсов и шаблона внедрения зависимостей позволяет разработчикам разъединять код, потому что в любой момент вы можете заменить реализацию на новую, реализующую этот интерфейс.
Еще одно преимущество примера развязки: теперь вы можете вызвать класс Payment из веб-запроса HTTP или командной строки, потому что вам нужно передать объект Money (обычно я пытаюсь передать типизированный объект или DTO) вместо объекта Request.
Связывание домена и фреймворка имеет темный побочный эффект, заключающийся в создании не обслуживаемого приложения.
Под «поддерживаемостью» я имею в виду отсутствие (уменьшение) технического долга.
Технический долг — это долг, который мы платим за наши (плохие) решения, и возвращается он нашим разочарованием и временем.
Поддерживаемое приложение — это приложение, которое увеличивает технический долг настолько медленными темпами, насколько это можно реально достичь.
Каковы характеристики легко обслуживаемого приложения?
Чтобы меньше трогать код для нового или существующего функционала, важно соблюдать принцип единственной ответственности для всех классов.
Хорошей концепцией является единственная ответственность за код, но она существует также и для архитектуры: какие изменения по той же причине следует сгруппировать, например:
Таким образом, мы можем создать самое важное различие в нашем проекте: домен, приложение и инфраструктуру.
Для домена я имею в виду:
Для приложения это:
Для инфраструктуры подразумевается:
На самом деле сторон может быть множество. И количество сторон говорит о том сколько “портов ввода-вывода” имеет наше приложение.
Каждый порт может использоваться адаптерами, чтобы наша система работала нормально.
Давайте подробно объясним, что означают порты и адаптеры.
Порты похожи на контракты, поэтому они не будут представлены в кодовой базе.
Существует порт для каждого способа вызова сценария использования приложения (через UI, API и т.д.), а также для всех способов выхода данных из приложения (персистентность, уведомления в другие системы и т. д.). Алистер Коберн называет эти порты первичным и вторичным или обычно разработчики называют их портами ввода и вывода.
Первичный и вторичный — это различие между намерением общения и поддерживающей реализацией.
Пример порта:
interface ProductRepositoryInterface
{
public function find(ProductId $id): ?Product;
}
Порты — это всего лишь определение того, что мы хотим делать. Они не говорят, как их достичь.
Адаптеры — это реализация портов, потому что для каждого из этих абстрактных портов нам нужен код, чтобы соединение работало.
Они очень конкретны и содержат низкоуровневый код и по определению не связаны со своими портами.
Пример адаптера:
class MysqlProductRepository implements ProductRepositoryInterface
{
private $repository;
public function __construct(ProductRepository $repository)
{
$this->repository = $repository;
}
public function find(ProductId $id): ?Product
{
return $this->repository->find(id);
}
}
Попробуем представить наши порты и адаптеры внутри реальной системы.
Как видите, у нас есть команда CLI или HTTP-запрос, которые вызывают наши входные адаптеры внутри уровня инфраструктуры. Адаптеры реализуют наши входные порты внутри уровня домена.
С другой стороны, у нас есть наши выходные адаптеры внутри уровня инфраструктуры, которые реализуют наши выходные порты внутри домена и могут взаимодействовать с внешней системой, такой как база данных.
Итак, в нашем приложении PHP у нас может быть такая структура директорий:
В этом примере у вас есть два разных контекста: Payment и Cart.
Здесь под каждым контекстом существует различие между доменом, приложением и инфраструктурой. Не обязательно иметь все эти каталоги, иногда может отсутствовать уровень приложения или уровень инфраструктуры.
В вашем домене у вас есть логика домена без зависимостей от каких-либо поставщиков (не всегда верно, например я обычно в своем домене использую генератор UUID ramsey/uuid).
Внутри этой директории у вас также есть все порты для указания того, как использовать эти данные с помощью объектов.
В папке вашего приложения вы можете иметь службы и сценарии использования.
В вашей инфраструктурной папке вы можете иметь код фреймворка и адаптеры, поэтому реализовать доменные порты можно с использованием любых библиотек и технологий.
Теперь, если объединить гексагональную архитектуру с принципом инверсии зависимостей, тогда вы еще больше улучшите свои проекты.
Принцип инверсии зависимостей означает, что модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.
Таким образом, класс инфраструктуры может зависеть от класса приложения и класса домена.
Класс приложения может зависеть от класса предметной области, но не может зависеть от класса инфраструктуры.
Доменный класс не может зависеть от класса инфраструктуры или приложения.
Как по мне, использование гексагональной архитектуры дает множество преимуществ:
На данный момент я стараюсь использовать эту архитектуру всегда, потому что, если начинаешь думать с таким мышлением, тогда очень трудно вернуться к старым практикам.
Обычно с устаревшим приложением, которое не следует этой архитектуре, я предлагаю команде начать пробовать новые вещи, чтобы сделать домен и код лучше и понятнее.
Это начинается с создания новых директорий, таких как Infrastructure и Domain.
После этого новые замыслы и фичи могут быть разработаны в этих директориях.
Со старым функционалом, если это возможно и не так сложно, я стараюсь создавать pull-реквесты переносящие небольшие части на новую архитектуру.
Когда я переношу старый legacy фрагмент кода я стараюсь следовать золотому правилу, которое я люблю, это правило бойскаута:
Оставьте код более чистым чем он был до того, как вы его нашли.
Для улучшения вашего домена и вашего кода я могу подсказать использовать:
Все эти концепции, методологии и подходы могут снова сделать ваши проекты еще более лучшими.
OpenAI готовится запустить собственную поисковую систему на базе ChatGPT. Информацию об этом публикуют западные издания. Ожидается, что новый поисковик может…
Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…
Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…
Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…
Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…
У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…