Ответ
Гексагональная архитектура, также известная как 'Порты и Адаптеры' (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): Требует создания большого количества интерфейсов, структур и слоев, что может быть избыточным для простых проектов.
- Высокий порог входа: Команде нужно понимать принципы архитектуры, чтобы правильно ее применять и не нарушать границы слоев.
Этот подход особенно ценен в сложных, долгоживущих системах, где требования к инфраструктуре могут меняться со временем, а бизнес-логика остается относительно стабильной.