Сталкивался ли с микросервисами, построенными по принципу гексагональной архитектуры?

Ответ

Да, работал над несколькими проектами, где микросервисы были построены по принципу гексагональной архитектуры (Ports & Adapters). Основная цель — изолировать бизнес-логику (ядро) от внешних зависимостей (БД, API, фреймворки).

Реализация в PHP-микросервисе (упрощенная структура):

src/
├── Domain/              # Ядро
│   ├── Model/
│   │   └── Order.php    # Сущность с бизнес-правилами
│   └── Repository/      
│       └── OrderRepositoryInterface.php # Порт
├── Application/         # Сценарии использования
│   └── Service/
│       └── OrderService.php
└── Infrastructure/      # Адаптеры
    ├── Persistence/
    │   └── DoctrineOrderRepository.php # Адаптер для Doctrine
    └── Api/
        └── PaymentGatewayAdapter.php   # Адаптер для внешнего API

Конкретный пример порта и адаптера для работы с платежами:

// Domain/Port/PaymentServiceInterface.php (Порт)
interface PaymentServiceInterface {
    public function charge(Order $order, Money $amount): TransactionId;
}

// Infrastructure/Adapter/StripePaymentAdapter.php (Адаптер)
class StripePaymentAdapter implements PaymentServiceInterface {
    private StripeClient $stripe;

    public function charge(Order $order, Money $amount): TransactionId {
        $charge = $this->stripe->charges->create([
            'amount' => $amount->getCents(),
            'currency' => $amount->getCurrency(),
            'source' => $order->getPaymentToken(),
            'description' => sprintf('Order #%s', $order->getId())
        ]);

        return new TransactionId($charge->id);
    }
}

// Application/Service/OrderProcessor.php (Сценарий использования)
class OrderProcessor {
    private PaymentServiceInterface $paymentService;

    public function process(Order $order): void {
        // Бизнес-логика не зависит от Stripe
        $transactionId = $this->paymentService->charge($order, $order->getTotal());
        $order->markAsPaid($transactionId);
    }
}

Преимущества в контексте микросервисов:

  • Тестируемость: Ядро можно тестировать юнит-тестами с моками адаптеров.
  • Замена инфраструктуры: Переход с Stripe на PayPal потребует только нового адаптера, без изменений в бизнес-логике.
  • Чистые границы: Четкое разделение ответственности между командами, работающими над разными сервисами.

Сложности:

  • Большой объем шаблонного кода для простых CRUD-операций.
  • Требует высокой дисциплины от команды, чтобы не допускать "утечек" инфраструктуры в доменный слой.

Ответ 18+ 🔞

Слушай, а я как-то вляпался в проект с этой самой гексагональной архитектурой, или, как её ещё обзывают, «порты и адаптеры». Ну, ёпта, идея-то в принципе здравая — отгородить свою священную бизнес-логику от всего этого внешнего ада, типа баз данных, апишек и прочего фреймворкового дерьма. Чтобы ядро было чистым, как слеза младенца, и ни от кого не зависело. Звучит как сказка, да? На практике же иногда получается такой овердохуища слоёный пирог из кода, что диву даёшься.

Вот смотри, как это примерно выглядит в PHP-сервисе. Представь себе структуру папок:

src/
├── Domain/              # Вот это наше святое-святых, ядро
│   ├── Model/
│   │   └── Order.php    # Сущность заказа со всей её бизнес-мудью
│   └── Repository/
│       └── OrderRepositoryInterface.php # А это порт, типа дверь наружу
├── Application/         # Тут живут сценарии использования
│   └── Service/
│       └── OrderService.php
└── Infrastructure/      # А вот здесь уже вся грязь реального мира
    ├── Persistence/
    │   └── DoctrineOrderRepository.php # Адаптер, который знает, как ебаться с Doctrine
    └── Api/
        └── PaymentGatewayAdapter.php   # А этот знает, как общаться со сторонним платежным гномом

Теперь конкретный пример, чтобы не быть голословным. Допустим, нам надо списать деньги. В ядре мы объявляем интерфейс — это наш порт. Типа договор: «чувак, кто бы ты ни был, ты должен уметь делать charge».

// Domain/Port/PaymentServiceInterface.php (Порт)
interface PaymentServiceInterface {
    public function charge(Order $order, Money $amount): TransactionId;
}

А в инфраструктуре сидит адаптер, который этот контракт выполняет, но делает это через какую-то конкретную хрень, например, Stripe.

// Infrastructure/Adapter/StripePaymentAdapter.php (Адаптер)
class StripePaymentAdapter implements PaymentServiceInterface {
    private StripeClient $stripe;

    public function charge(Order $order, Money $amount): TransactionId {
        // Вся эта муть с вызовом Stripe API — она ЗДЕСЬ, а не в ядре!
        $charge = $this->stripe->charges->create([
            'amount' => $amount->getCents(),
            'currency' => $amount->getCurrency(),
            'source' => $order->getPaymentToken(),
            'description' => sprintf('Order #%s', $order->getId())
        ]);

        return new TransactionId($charge->id);
    }
}

И вот красота: наш сервис приложения, где живёт логика обработки заказа, нихрена не знает про Stripe. Он просто тыкает в порт.

// Application/Service/OrderProcessor.php
class OrderProcessor {
    private PaymentServiceInterface $paymentService; // Зависим от абстракции, ёпта!

    public function process(Order $order): void {
        // Главная магия: бизнес-логика чиста. Она просто говорит: «списать деньги».
        $transactionId = $this->paymentService->charge($order, $order->getTotal());
        $order->markAsPaid($transactionId); // И отмечает заказ оплаченным.
    }
}

И в чём, собственно, профит, спросишь ты? А профит, чувак, есть, и немаленький.

  • Тестируемость — просто пиздец. Хочешь потестить ядро? Подсовываешь ему заглушку (mock) вместо адаптера, и всё. Никаких реальных платежей, никаких падений БД. Чистые юнит-тесты, которые летают.
  • Замена инфраструктуры. Надоел Stripe со своими комиссиями? Решили переехать на PayPal? Да похуй! Пишем новый PayPalPaymentAdapter, который реализует тот же самый интерфейс, подменяем его в контейнере зависимостей — и вся бизнес-логика даже не чихнёт. Она продолжает вызывать charge, а что там под капотом — её не ебёт.
  • Чёткие границы. Когда в команде несколько микросервисов, сразу понятно, кто за что отвечает. Ребята с ядра не лезут в инфраструктуру, и наоборот. Меньше шансов накосячить и насрать в чужой огород.

Но не всё так гладко, блядь. Сложности тоже есть, и они порой доверия ебать ноль оставляют.

  • Объём шаблонного кода — овердохуища. Для простенькой CRUD-операции нужно создать интерфейс, его реализацию, прописать всё в DI… Иногда кажется, что ты пишешь не логику, а папки и файлы. Для маленького сервиса это может быть избыточно, как из пушки по воробьям.
  • Требует железной дисциплины. Команда должна чётко понимать правила и не допускать «утечек». Стоит одному ленивому челу заюзать в доменном слое класс из Doctrine или какую-нибудь библиотеку — и всё, архитектура накрылась медным тазом. Изоляция нарушена, и смысл всей этой затеи идёт лесом. Нужны строгие code review и чтобы у всех в голове было понимание, что к чему.