Из чего состоит гексагональная архитектура
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Гексагональная архитектура
Гексагональная архитектура (Hexagonal Architecture или Ports and Adapters) — это архитектурный паттерн, разработанный Алистаром Кокберном. Он направлен на создание слабо связанных, тестируемых и независимых от внешних зависимостей систем. Несмотря на название, количество слоев не имеет отношения к числу 6 — гексагон символизирует универсальность со всех сторон.
Основная идея
Центральная идея: приложение не должно зависеть от внешних систем. Вместо этого внешние системы присоединяются через порты и адаптеры.
┌────────────────────────────────────┐
│ Web UI (Adapter) │
│ Mobile UI (Adapter) │
│ CLI (Adapter) │
└────────────┬───────────────────────┘
│
┌────▼─────────────────────┐
│ PORTS (interfaces) │
└────────────────────────┬─┘
│
┌────────▼──────────────────┐
│ APPLICATION CORE │
│ (Business Logic) │
└────────┬──────────────────┘
│
┌────▼─────────────────────┐
│ PORTS (interfaces) │
└────────────────────────┬─┘
│
┌────────────┴───────────────────────┐
│ Database (Adapter) │
│ Email Service (Adapter) │
│ Payment Service (Adapter) │
└────────────────────────────────────┘
Основные компоненты
1. Application Core (Ядро приложения)
Это сердце системы, которое содержит всю бизнес-логику, независимую от деталей реализации.
// domain/User.js — Entity
class User {
constructor(id, email, name) {
this.id = id;
this.email = email;
this.name = name;
}
isValidEmail() {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
}
}
// application/CreateUserUseCase.js
class CreateUserUseCase {
constructor(userRepository, emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
async execute(userData) {
const user = new User(null, userData.email, userData.name);
if (!user.isValidEmail()) {
throw new Error("Invalid email");
}
const savedUser = await this.userRepository.save(user);
await this.emailService.sendWelcome(savedUser.email);
return savedUser;
}
}
Главное: Ядро не знает, как хранятся данные или как отправляются письма.
2. Ports (Порты)
Порты — это интерфейсы, которые определяют, как ядро приложения взаимодействует с внешним миром. Существует два типа портов:
Входящие порты (Driving Ports) — как внешние системы используют приложение:
// ports/CreateUserPort.js
class CreateUserPort {
async execute(userData) {
throw new Error("Must be implemented");
}
}
// Реальная реализация в ядре
class CreateUserUseCase extends CreateUserPort {
// ...
}
Исходящие порты (Driven Ports) — как приложение общается с внешними системами:
// ports/UserRepositoryPort.js
class UserRepositoryPort {
async save(user) {
throw new Error("Must be implemented");
}
async findById(id) {
throw new Error("Must be implemented");
}
}
// ports/EmailServicePort.js
class EmailServicePort {
async sendWelcome(email) {
throw new Error("Must be implemented");
}
}
3. Adapters (Адаптеры)
Адаптеры реализуют порты и создают мосты между ядром и внешними системами.
Входящие адаптеры (как пользователи общаются с приложением):
// adapters/web/UserController.js
class UserController {
constructor(createUserUseCase) {
this.createUserUseCase = createUserUseCase;
}
async create(req, res) {
try {
const user = await this.createUserUseCase.execute(req.body);
res.json({ success: true, user });
} catch (error) {
res.status(400).json({ error: error.message });
}
}
}
// adapters/cli/UserCLI.js
class UserCLI {
constructor(createUserUseCase) {
this.createUserUseCase = createUserUseCase;
}
async run(args) {
const user = await this.createUserUseCase.execute({
email: args.email,
name: args.name
});
console.log(`User created: ${user.name}`);
}
}
Исходящие адаптеры (как приложение общается с внешними системами):
// adapters/database/PostgresUserRepository.js
const { UserRepositoryPort } = require("../../ports");
class PostgresUserRepository extends UserRepositoryPort {
constructor(db) {
super();
this.db = db;
}
async save(user) {
const result = await this.db.query(
"INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *",
[user.email, user.name]
);
return result.rows[0];
}
async findById(id) {
const result = await this.db.query(
"SELECT * FROM users WHERE id = $1",
[id]
);
return result.rows[0];
}
}
// adapters/email/GmailEmailService.js
const { EmailServicePort } = require("../../ports");
class GmailEmailService extends EmailServicePort {
constructor(gmailAPI) {
super();
this.gmailAPI = gmailAPI;
}
async sendWelcome(email) {
await this.gmailAPI.send({
to: email,
subject: "Welcome!",
body: "Thank you for signing up"
});
}
}
// Альтернативная реализация для смс
class TwilioEmailService extends EmailServicePort {
constructor(twilioAPI) {
super();
this.twilioAPI = twilioAPI;
}
async sendWelcome(email) {
await this.twilioAPI.sendSMS({
to: email,
body: "Welcome!"
});
}
}
4. Dependency Injection (Внедрение зависимостей)
Адаптеры и порты соединяются через инъекцию зависимостей:
// main.js или bootstrap
const db = new PostgreSQL();
const userRepository = new PostgresUserRepository(db);
const gmailAPI = new GmailAPI();
const emailService = new GmailEmailService(gmailAPI);
const createUserUseCase = new CreateUserUseCase(
userRepository,
emailService
);
const userController = new UserController(createUserUseCase);
// Позже, если нужна другая реализация
const mockRepository = new InMemoryUserRepository();
const mockEmailService = new InMemoryEmailService();
const testUseCase = new CreateUserUseCase(
mockRepository,
mockEmailService
);
Преимущества гексагональной архитектуры
1. Независимость от фреймворков
// Приложение работает так же с Express, Fastify или другим фреймворком
const controller = new UserController(createUserUseCase);
// Express
app.post('/users', controller.create.bind(controller));
// Fastify
fastify.post('/users', controller.create.bind(controller));
2. Легко тестировать
// Тест без базы данных и почты
class InMemoryUserRepository extends UserRepositoryPort {
async save(user) {
return { ...user, id: 1 };
}
}
class MockEmailService extends EmailServicePort {
async sendWelcome(email) {
// Do nothing
}
}
const useCase = new CreateUserUseCase(
new InMemoryUserRepository(),
new MockEmailService()
);
test('should create user', async () => {
const user = await useCase.execute({
email: 'test@example.com',
name: 'John'
});
expect(user.id).toBeDefined();
});
3. Легко заменять реализации
// Было: PostgreSQL
const repository = new PostgresUserRepository(db);
// Стало: MongoDB
const repository = new MongoUserRepository(db);
// Бизнес-логика работает одинаково
const useCase = new CreateUserUseCase(repository, emailService);
4. Четкая структура проекта
src/
domain/ # Entities, Value Objects
User.js
Role.js
application/ # Use Cases
CreateUserUseCase.js
DeleteUserUseCase.js
ports/ # Interfaces
UserRepositoryPort.js
EmailServicePort.js
adapters/
web/ # HTTP Controllers
UserController.js
database/ # Database implementations
PostgresUserRepository.js
email/ # Email implementations
GmailEmailService.js
TwilioEmailService.js
cli/ # CLI handlers
UserCLI.js
Когда использовать
Используй гексагональную архитектуру для:
- Крупных, долгоживущих проектов
- Систем, которые должны взаимодействовать с разными БД или сервисами
- Когда нужна высокая тестируемость
- Команд, работающих параллельно на разных адаптерах
Не используй, если:
- Простое CRUD приложение
- Макет, MVP
- Один разработчик работает недолго
Заключение
Гексагональная архитектура — это мощный паттерн для создания гибких, тестируемых и поддерживаемых систем. Она обеспечивает полную независимость бизнес-логики от деталей реализации, позволяя легко менять адаптеры без изменения ядра приложения. Это особенно важно для долгосрочных проектов, где требования и технологии постоянно меняются.