В чем суть гексагональной архитектуры (порты и адаптеры)

Ответ

Гексагональная архитектура (Hexagonal Architecture), также известная как «Порты и Адаптеры», — это архитектурный паттерн, главная цель которого — изолировать доменную (бизнес-) логику приложения от внешних зависимостей, таких как UI, базы данных, сторонние API и фреймворки.

Ключевые компоненты:

  • Ядро (Core/Domain): Центральная часть приложения, содержащая чистую бизнес-логику. Она не имеет зависимостей от внешних технологий.
  • Порты (Ports): Это интерфейсы (абстрактные классы в Python), которые определяют контракты для взаимодействия с ядром. Делятся на два типа:
    • Входящие (Driving) порты: API, который ядро предоставляет внешнему миру (например, интерфейс сервиса для обработки запросов).
    • Исходящие (Driven) порты: Интерфейсы, которые ядро использует для получения данных извне (например, интерфейс репозитория для работы с БД).
  • Адаптеры (Adapters): Конкретные реализации портов, которые «переводят» запросы из внешнего мира на язык ядра и наоборот.
    • Входящие адаптеры: REST-контроллеры, gRPC-серверы, консольные команды.
    • Исходящие адаптеры: ORM-репозитории, клиенты для внешних API, файловые хранилища.

Пример на Python:

from abc import ABC, abstractmethod

# --- Исходящий Порт (Driven Port) ---
class UserRepository(ABC):
    @abstractmethod
    def save(self, user_name: str):
        pass

# --- Исходящий Адаптер (Driven Adapter) ---
class PostgresUserRepository(UserRepository):
    def save(self, user_name: str):
        # Логика сохранения в PostgreSQL
        print(f"Пользователь '{user_name}' сохранен в PostgreSQL.")

# --- Ядро Приложения (Core) ---
class UserService:
    # Ядро зависит только от абстракции (порта), а не от конкретной реализации
    def __init__(self, user_repo: UserRepository):
        self._user_repo = user_repo

    def register_user(self, user_name: str):
        # Бизнес-логика
        if len(user_name) < 3:
            raise ValueError("Имя пользователя слишком короткое")
        self._user_repo.save(user_name)

# --- Входящий Адаптер (Driving Adapter) - условный REST-контроллер ---
if __name__ == "__main__":
    # В точке входа мы собираем зависимости: ядро и конкретный адаптер
    db_adapter = PostgresUserRepository()
    user_service = UserService(user_repo=db_adapter)

    # Внешний мир (например, HTTP-запрос) вызывает сервис
    user_service.register_user("Alice")

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

  • Высокая тестируемость: Ядро можно тестировать в полной изоляции, подменяя адаптеры моками.
  • Заменяемость технологий: Можно легко заменить базу данных (например, с PostgreSQL на MongoDB), просто написав новый адаптер, не трогая бизнес-логику.
  • Независимость от фреймворков: Бизнес-логика не привязана к конкретному веб-фреймворку или ORM.

Ответ 18+ 🔞

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

Представь себе, блядь, шестиугольник. В центре — святая святых, ядро. Это твоя бизнес-логика, чистая, как слеза младенца. Она нихуя не знает про то, что там за бортом творится. Никаких «импортов от фласка» или «сессий от алхимии» внутрь не пролезет. Абсолютная девственность, ёпта!

А вокруг этого ядра — порты. Это как дырки в стене, блядь. Через них ядро общается с внешним миром, но только по своим, чётко прописанным правилам. Их два вида:

  • Входящие порты: Это когда внешний мир (какой-нибудь долбоёб с HTTP-запросом) стучится к тебе в дверь и говорит: «Эй, зарегистрируй пользователя!». Ядро ему выкидывает контракт: «Ладно, мудила, но только вот таким методом и с такими аргументами».
  • Исходящие порты: А это когда само ядро, выполняя свою умную работу, понимает: «Бля, мне надо пользователя в базу сохранить». Но вместо того чтобы тупо вызвать session.add(), оно орёт в дыру в стене: «Эй, кто там с базой работает? Сохрани-ка мне эту сущность!». И ждёт, что кто-то откликнется по заранее оговорённому интерфейсу.

Ну а адаптеры — это те самые уроды, которые стоят по ту сторону стены и переводят с языка улиц на язык дворца. Входящий адаптер — это твой REST-контроллер, который взял JSON от какого-то левого запроса, разобрал его и красиво постучал во входящий порт. А исходящий адаптер — это твой репозиторий на SQLAlchemy, который, услышав крик ядра из исходящего порта, бежит и делает INSERT INTO users ....

Смотри, как это выглядит в коде, блядь (код не трогаю, он святой):

from abc import ABC, abstractmethod

# --- Исходящий Порт (Driven Port) ---
class UserRepository(ABC):
    @abstractmethod
    def save(self, user_name: str):
        pass

# --- Исходящий Адаптер (Driven Adapter) ---
class PostgresUserRepository(UserRepository):
    def save(self, user_name: str):
        # Логика сохранения в PostgreSQL
        print(f"Пользователь '{user_name}' сохранен в PostgreSQL.")

# --- Ядро Приложения (Core) ---
class UserService:
    # Ядро зависит только от абстракции (порта), а не от конкретной реализации
    def __init__(self, user_repo: UserRepository):
        self._user_repo = user_repo

    def register_user(self, user_name: str):
        # Бизнес-логика
        if len(user_name) < 3:
            raise ValueError("Имя пользователя слишком короткое")
        self._user_repo.save(user_name)

# --- Входящий Адаптер (Driving Adapter) - условный REST-контроллер ---
if __name__ == "__main__":
    # В точке входа мы собираем зависимости: ядро и конкретный адаптер
    db_adapter = PostgresUserRepository()
    user_service = UserService(user_repo=db_adapter)

    # Внешний мир (например, HTTP-запрос) вызывает сервис
    user_service.register_user("Alice")

Видишь? UserService (ядро) нихуя не знает про PostgreSQL. Ему похуй. Ему подсунули какую-то хрень, которая умеет .save(), и он её использует. А завтра, если начальство скажет «переезжаем на MongoDB, блядь», ты просто создашь новый класс MongoUserRepository, который тоже implements этот порт UserRepository, и подсунешь его в сервис. И ядро даже не чихнёт! Оно продолжит работать, как ни в чём не бывало. Волшебство, ёпта!

В чём, сука, профит?

  1. Тестируемость — овердохуищная. Хочешь протестировать бизнес-логику? Подсовываешь ядру вместо реального репозитория какую-нибудь заглушку (mock), которая нихуя не делает, и проверяешь, правильно ли оно там валидирует и орёт. Никаких поднятий баз данных, никаких фреймворков — чистая логика.
  2. Заменяемость технологий — пиздец. Надоел PostgreSQL? Заебал Django? В рот их чих-пых! Выкидываешь старые адаптеры и пишешь новые. Ядро, твоя самая ценная часть, остаётся нетронутой, как музейный экспонат.
  3. Независимость от фреймворков. Твоя бизнес-логика не становится заложником очередного модного веб-фреймворка, который через два года выйдет из моды. Она живёт в своём уютном мирке, а все эти шлюхи-адаптеры бегают вокруг и обслуживают её.

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