Как в гексагональной архитектуре разместить клиентское приложение на React?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
React в гексагональной архитектуре
Что такое гексагональная архитектура?
Гексагональная архитектура (Hexagonal Architecture или Ports & Adapters) — это паттерн, где бизнес-логика находится в центре ядра (domain), а всё остальное (UI, БД, внешние сервисы) — это adaptors на периметре.
┌────────────────────────────────────────────────┐
│ Ports (границы) │
│ ┌─────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Domain (бизнес-логика) │ │ │
│ │ │ - Use Cases │ │ │
│ │ │ - Entities │ │ │
│ │ │ - Value Objects │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ ▲ ▲ ▲ ▲ ▲ │
│ │ Port 1 │ Port 2 │ Port 3 │ Port N │ │
└──┼────────┼────────┼────────┼────────┼─┘
│ │ │ │ │
┌──▼──────┬─▼──────┬─▼──────┬─▼──────┬─▼──────────┐
│ Adapter │Adapter │Adapter │Adapter │ Adapter │
│ (UI) │ (HTTP) │ (DB) │ (Cache)│ (Queue) │
└─────────┴────────┴────────┴────────┴────────────┘
Позиция React в этой архитектуре
React — это Adapter (UI Adapter), а не часть domain. React находится на периметре гексагона, отделённый от бизнес-логики портом.
Структура папок
project/
├── src/
│ │
│ ├── domain/ # Ядро: бизнес-логика
│ │ ├── entities/ # Основные сущности
│ │ │ ├── User.ts
│ │ │ ├── Product.ts
│ │ │ └── Order.ts
│ │ ├── use-cases/ # Use cases (interactors)
│ │ │ ├── AuthenticateUser.ts
│ │ │ ├── CreateOrder.ts
│ │ │ └── GetUserProfile.ts
│ │ ├── repositories/ # Интерфейсы (ports)
│ │ │ ├── IUserRepository.ts
│ │ │ ├── IProductRepository.ts
│ │ │ └── IOrderRepository.ts
│ │ └── value-objects/ # Value objects
│ │ ├── Email.ts
│ │ ├── Price.ts
│ │ └── OrderStatus.ts
│ │
│ ├── application/ # Application layer
│ │ ├── dtos/ # Data Transfer Objects
│ │ │ ├── CreateUserDTO.ts
│ │ │ ├── UserResponseDTO.ts
│ │ │ └── OrderDTO.ts
│ │ └── services/ # Бизнес-сервисы
│ │ └── UserService.ts
│ │
│ ├── infrastructure/ # Adaptors (реализация портов)
│ │ ├── persistence/ # БД adapter
│ │ │ ├── DatabaseUserRepository.ts
│ │ │ ├── DatabaseProductRepository.ts
│ │ │ └── index.ts
│ │ ├── http/ # HTTP adapter (for backend)
│ │ │ ├── HttpUserRepository.ts
│ │ │ ├── client.ts # Fetch/Axios instance
│ │ │ └── index.ts
│ │ └── cache/ # Cache adapter
│ │ └── LocalStorageCache.ts
│ │
│ ├── presentation/ # React Adapter (UI)
│ │ ├── components/ # React компоненты
│ │ │ ├── UserProfile.tsx
│ │ │ ├── OrderForm.tsx
│ │ │ └── ProductList.tsx
│ │ ├── hooks/ # Custom hooks (connect domain to React)
│ │ │ ├── useAuth.ts
│ │ │ ├── useOrders.ts
│ │ │ └── useProducts.ts
│ │ ├── pages/ # Next.js pages или App pages
│ │ │ ├── /profile
│ │ │ ├── /orders
│ │ │ └── /checkout
│ │ ├── contexts/ # React Context (state management)
│ │ │ ├── AuthContext.tsx
│ │ │ └── OrderContext.tsx
│ │ └── lib/ # Утилиты UI слоя
│ │ ├── api-config.ts
│ │ └── ui-utils.ts
│ │
│ └── main.tsx # Точка входа React
│
└── package.json
Пример: Контакт между слоями
1. Domain слой (бизнес-логика)
// domain/entities/User.ts
export class User {
constructor(
public id: string,
public name: string,
public email: string,
public role: "admin" | "user"
) {}
isAdmin(): boolean {
return this.role === "admin";
}
updateProfile(name: string): User {
return new User(this.id, name, this.email, this.role);
}
}
// domain/use-cases/GetUserProfile.ts
export interface IUserRepository {
getUserById(id: string): Promise<User | null>;
updateUser(user: User): Promise<void>;
}
export class GetUserProfileUseCase {
constructor(private userRepository: IUserRepository) {}
async execute(userId: string): Promise<User> {
const user = await this.userRepository.getUserById(userId);
if (!user) {
throw new Error("User not found");
}
return user;
}
}
Note: domain слой НЕ знает о React, HTTP, БД. Он знает только об интерфейсах (ports).
2. Infrastructure слой (реализация портов)
// infrastructure/http/HttpUserRepository.ts
import { IUserRepository } from "@/domain/repositories/IUserRepository";
import { User } from "@/domain/entities/User";
export class HttpUserRepository implements IUserRepository {
constructor(private baseUrl: string) {}
async getUserById(id: string): Promise<User | null> {
const response = await fetch(`${this.baseUrl}/users/${id}`);
if (!response.ok) return null;
const data = await response.json();
return new User(data.id, data.name, data.email, data.role);
}
async updateUser(user: User): Promise<void> {
await fetch(`${this.baseUrl}/users/${user.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: user.name,
email: user.email,
role: user.role,
}),
});
}
}
Eту реализацию можно swap на другую (DatabaseUserRepository, CachedUserRepository), не меняя domain.
3. Presentation слой (React)
// presentation/hooks/useAuth.ts
import { useState, useEffect } from "react";
import { User } from "@/domain/entities/User";
import { GetUserProfileUseCase } from "@/domain/use-cases/GetUserProfile";
export function useAuth(userRepository: IUserRepository) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const useCase = new GetUserProfileUseCase(userRepository);
useCase
.execute("current-user-id")
.then(setUser)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [userRepository]);
return { user, loading, error };
}
// presentation/components/UserProfile.tsx
export function UserProfile() {
const { user, loading, error } = useAuth(userRepository);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>Email: {user?.email}</p>
{user?.isAdmin() && <p>Admin privileges</p>}
</div>
);
}
React компонент не знает как загружаются данные (HTTP, БД, cache). Он просто использует hook, который использует use case.
Dependency Injection для связи слоев
// main.tsx (точка входа)
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./presentation/App";
// 1. Создаём infrastructure
const userRepository = new HttpUserRepository("https://api.example.com");
const productRepository = new HttpProductRepository("https://api.example.com");
// 2. Инжектируем в контекст или провайдер
const services = {
userRepository,
productRepository,
};
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App services={services} />
</React.StrictMode>
);
// presentation/App.tsx
type AppProps = {
services: typeof services;
};
export function App({ services }: AppProps) {
return (
<AuthProvider userRepository={services.userRepository}>
<ProductProvider productRepository={services.productRepository}>
<Routes />
</ProductProvider>
</AuthProvider>
);
}
Почему так структурировать?
1. Независимость от framework
// Если захочу переойти с React на Vue/Svelte, domain всё-ещё работает
// Просто переписываю presentation слой
2. Легко тестировать
// Тестирую domain без React, без HTTP запросов
class MockUserRepository implements IUserRepository {
async getUserById(id: string): Promise<User | null> {
return new User("1", "Test User", "test@example.com", "user");
}
}
const useCase = new GetUserProfileUseCase(new MockUserRepository());
const user = await useCase.execute("1");
assert.equal(user.name, "Test User");
3. Легко менять реализацию
// Была HTTP, хочу перейти на IndexedDB
const userRepository = new IndexedDBUserRepository();
// Всё работает, domain не изменился
4. Масштабируемость
// Легко добавить новый adapter (кеш, WebSocket, SSE)
class CachedUserRepository implements IUserRepository {
constructor(
private database: HttpUserRepository,
private cache: LocalStorageCache
) {}
async getUserById(id: string): Promise<User | null> {
const cached = this.cache.get(`user-${id}`);
if (cached) return cached;
const user = await this.database.getUserById(id);
if (user) this.cache.set(`user-${id}`, user);
return user;
}
}
Правила слоёв (Dependency Direction)
presentation → application → domain ← infrastructure
↓ ↑
React Ports
UI (interfaces)
Важно: domain не зависит от presentation и infrastructure. Они зависят от domain.
Чек-лист применения
- Вся бизнес-логика в domain/ (entities, use-cases, value-objects)
- Интерфейсы (ports) в domain/repositories/
- Реализация в infrastructure/
- React компоненты в presentation/, не содержат бизнес-логики
- Dependency injection в main.tsx
- Domain легко тестировать без React
- Можно swap infrastructure без изменения domain
Выводы: React в гексагональной архитектуре — это UI adapter, отделённый от бизнес-логики через ports. Это обеспечивает гибкость, тестируемость и масштабируемость.