Ответ
Да, работал над несколькими проектами, где микросервисы были построены по принципу гексагональной архитектуры (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 и чтобы у всех в голове было понимание, что к чему.