Ответ
Гексагональная архитектура, также известная как 'Порты и Адаптеры' (Ports and Adapters), — это архитектурный паттерн, предназначенный для создания слабосвязанных компонентов приложения, которые легко тестировать и поддерживать. Основная идея — изолировать ядро с бизнес-логикой от внешних зависимостей, таких как базы данных, UI, внешние API и т.д.
Ключевые компоненты:
- Ядро (Hexagon): Содержит всю бизнес-логику и бизнес-правила приложения. Оно не должно иметь никаких зависимостей от внешних технологий (фреймворков, БД).
- Порты (Ports): Это интерфейсы, которые определяют, как ядро взаимодействует с внешним миром. Они являются частью ядра. В Go порты — это просто
interface. - Адаптеры (Adapters): Это конкретные реализации портов. Они служат мостом между ядром и внешними системами. Например, адаптер для базы данных реализует интерфейс репозитория, а HTTP-хендлер использует интерфейс сервиса из ядра.
В Go этот паттерн идеально ложится на использование интерфейсов и принципа инверсии зависимостей (Dependency Injection).
Пример реализации в Go:
// Определяем модель данных
type User struct {
ID int
Name string
}
// --- Ядро приложения ---
// Порт для работы с хранилищем пользователей (Driven Port)
type UserRepository interface {
Save(user User) error
GetByID(id int) (User, error)
}
// Сервис с бизнес-логикой
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) RegisterNewUser(name string) error {
// Здесь может быть сложная бизнес-логика: валидация, проверки и т.д.
user := User{Name: name}
return s.repo.Save(user)
}
// --- Адаптеры ---
// Адаптер для работы с PostgreSQL
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) Save(user User) error {
// Реализация сохранения пользователя в Postgres
// INSERT INTO users ...
return nil
}
func (r *PostgresUserRepository) GetByID(id int) (User, error) {
// Реализация получения пользователя из Postgres
// SELECT * FROM users WHERE id = ...
return User{}, nil
}
// Адаптер для входящих HTTP-запросов (Driving Adapter)
type UserHandler struct {
service *UserService
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
// Логика обработки HTTP-запроса
name := r.FormValue("name")
err := h.service.RegisterNewUser(name)
// ... обработка ответа
}
Преимущества:
- Высокая тестируемость и изоляция: Бизнес-логику можно тестировать полностью изолированно, подменяя реализации адаптеров на моки (in-memory реализации).
- Технологическая независимость: Ядро не зависит от конкретной БД, фреймворка или способа доставки сообщений. Можно заменить PostgreSQL на MongoDB, не меняя ни строчки в бизнес-логике.
- Гибкость и заменяемость: Адаптеры можно легко добавлять или заменять. Например, добавить CLI-интерфейс к приложению, который будет использовать те же сервисы, что и HTTP API.
Недостатки:
- Увеличение сложности (Boilerplate): Требует создания большого количества интерфейсов, структур и слоев, что может быть избыточным для простых проектов.
- Высокий порог входа: Команде нужно понимать принципы архитектуры, чтобы правильно ее применять и не нарушать границы слоев.
Этот подход особенно ценен в сложных, долгоживущих системах, где требования к инфраструктуре могут меняться со временем, а бизнес-логика остается относительно стабильной.
Ответ 18+ 🔞
А, гексагональная архитектура, ну да, эта самая «порт-адаптерная» хуйня! Слушай, это же просто способ не сойти с ума, когда твоё приложение превращается в такой спагетти-код, что любое изменение — это пиздец и волнение ебать.
Представь себе, блядь: у тебя есть ядро — это святая святых, там твоя бизнес-логика, твои правила. Оно должно быть чистым, как слеза младенца, и нихуя не знать про то, какая там база данных у тебя — PostgreSQL или MongoDB, и как к нему вообще обращаются — через HTTP, через консоль или через телепатию, ёпта!
А как это сделать? Да через интерфейсы, мать их! В Go это же родное. Вот смотри, как это выглядит в жизни, без всей этой заумной пурги.
// Это наша модель, тут всё просто
type User struct {
ID int
Name string
}
// --- Вот это наше ядро, блядь! ---
// Это порт, интерфейс. Говорит: "Я хочу уметь сохранять и получать пользователей, а КАК — мне похуй!"
type UserRepository interface {
Save(user User) error
GetByID(id int) (User, error)
}
// А это наш главный по тарелкам, сервис. Ему скармливают реализацию этого интерфейса.
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// И вот он, бизнес-процесс! Регистрация пользователя. Может быть сложная, с проверками.
func (s *UserService) RegisterNewUser(name string) error {
user := User{Name: name}
return s.repo.Save(user) // Сохраняет куда? Да куда угодно! Его это не ебёт!
}
А теперь, внимание, адаптеры! Это те самые уродцы, которые знают всю грязную правду о внешнем мире.
// Адаптер №1: Постгрес. Знает SQL и как воткнуть в него структуру.
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) Save(user User) error {
// Вот тут уже конкретика, INSERT INTO и всё такое
return nil
}
// ... GetByID тоже тут
// Адаптер №2: HTTP-хендлер. Знает, как выковыривать данные из запроса и пихать их в сервис.
type UserHandler struct {
service *UserService
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
err := h.service.RegisterNewUser(name) // А сервису всё равно, откуда имя пришло!
// ... дальше формируем ответ
}
И что мы имеем в сухом остатке, а? Преимущества:
- Тестировать — одно удовольствие. Хочешь проверить логику? Подсовываешь сервису нахуй какую-нибудь заглушку-репозиторий, которая в памяти всё хранит, и тестируешь без всяких баз данных. Удивление пиздец, как быстро всё работает!
- Менять технологии — не больно. Надоел Postgres? Захотел MongoDB? Пиздуй — пишешь новый адаптер
MongoUserRepository, который реализует тот же интерфейсUserRepository, и подменяешь его в инициализации. Ядро даже не чихнёт! - Добавлять точки входа — легко. Сделал HTTP API? Отлично. Захотел ещё и CLI-утилиту? Да пожалуйста! Пишешь адаптер для командной строки, который использует тот же самый
UserService. Бизнес-логика одна на всех, красота.
Но и недостатки, куда же без них, блядь:
- Бойлерплейта — овердохуища. Интерфейсы на каждый чих, структуры, слои... Для маленькой утилиты на три кнопки — это стрельба из пушки по воробьям, чистое выёбывание.
- Надо думать головой. Команда должна понимать, где проходит граница, что куда можно пихать. А то получится «гексагональная архитектура», где хендлер лезет напрямую в базу, минуя все порты. Пизда тогда проекту.
Короче, инструмент мощный, но не для каждой задачи. Бери его, когда чувствуешь, что проект будет жить долго и обрастёт кучей зависимостей, которые ещё двадцать раз поменяются. А для прототипа или микросервиса-однодневки — может, и не стоит так заморачиваться, чих-пых тебя в сраку.