Антон Ларичев

Введение
Чистая архитектура (Clean Architecture) — это набор принципов проектирования, предложенный Робертом Мартином в 2012 году. Её цель — создать систему, в которой бизнес-логика не зависит от фреймворков, баз данных и внешних сервисов. Для веб-разработчика это означает возможность поменять Express на Fastify, PostgreSQL на MongoDB или REST на GraphQL без переписывания ядра приложения.
В статье разберём основные концепции, реализуем пример на TypeScript и рассмотрим типичные ошибки при внедрении.
Основные принципы и слои
Clean Architecture строится вокруг одного ключевого правила — правила зависимостей: зависимости в коде могут указывать только внутрь, к более высокоуровневым политикам. Архитектура разделена на четыре концентрических слоя.
Entities — сущности
Самый внутренний слой. Содержит бизнес-объекты и правила, которые не зависят ни от чего внешнего. Это ядро приложения.
export class User {
private constructor(
public readonly id: string,
public readonly email: string,
private readonly password: string
) {}
// Фабричный метод защищает инварианты на уровне домена
static create(id: string, email: string, password: string): User {
if (!email.includes('@')) {
throw new Error('Некорректный email');
}
if (password.length < 8) {
throw new Error('Пароль слишком короткий');
}
return new User(id, email, password);
}
}
Use Cases — сценарии использования
Второй слой содержит логику приложения — конкретные действия, которые пользователь может совершить. Use Cases оркестрируют сущности и обращаются к внешнему миру через интерфейсы (порты).
// Порт — интерфейс, который реализует внешний слой
export interface UserRepository {
save(user: User): Promise<void>;
findByEmail(email: string): Promise<User | null>;
}
export class RegisterUserUseCase {
constructor(private readonly userRepository: UserRepository) {}
async execute(email: string, password: string): Promise<void> {
// Проверяем уникальность email перед созданием
const existing = await this.userRepository.findByEmail(email);
if (existing) {
throw new Error('Пользователь с таким email уже существует');
}
const user = User.create(crypto.randomUUID(), email, password);
await this.userRepository.save(user);
}
}
Interface Adapters — адаптеры интерфейса
Третий слой преобразует данные между форматом Use Cases и внешним миром. Здесь живут контроллеры, презентеры и реализации репозиториев.
// Реализация репозитория через Prisma — знает о БД, но не знает о HTTP
export class PrismaUserRepository implements UserRepository {
constructor(private readonly prisma: PrismaClient) {}
async save(user: User): Promise<void> {
await this.prisma.user.create({
data: { id: user.id, email: user.email },
});
}
async findByEmail(email: string): Promise<User | null> {
const record = await this.prisma.user.findUnique({ where: { email } });
if (!record) return null;
// Восстанавливаем доменную сущность из записи БД
return User.create(record.id, record.email, record.password);
}
}
// HTTP-контроллер — адаптер между запросом и Use Case
export class UserController {
constructor(private readonly registerUser: RegisterUserUseCase) {}
async register(req: Request, res: Response): Promise<void> {
try {
await this.registerUser.execute(req.body.email, req.body.password);
res.status(201).json({ message: 'Пользователь создан' });
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
}
}
Frameworks and Drivers — фреймворки и драйверы
Самый внешний слой. Здесь конфигурация Express, подключение к БД, настройка DI-контейнера. Этот слой знает обо всех остальных, но остальные о нём — нет.
// Точка входа — ручная сборка зависимостей (Composition Root)
const prisma = new PrismaClient();
const userRepository = new PrismaUserRepository(prisma);
const registerUserUseCase = new RegisterUserUseCase(userRepository);
const userController = new UserController(registerUserUseCase);
const app = express();
app.use(express.json());
app.post('/users/register', (req, res) => userController.register(req, res));
app.listen(3000);
Структура папок
Хорошая структура папок отражает слои архитектуры, а не технические детали фреймворка:
src/
├── domain/ # Entities
│ └── user/
│ └── user.entity.ts
├── application/ # Use Cases и порты
│ └── user/
│ ├── register-user.use-case.ts
│ └── user.repository.ts
├── infrastructure/ # Адаптеры и фреймворки
│ ├── database/
│ │ └── prisma-user.repository.ts
│ └── http/
│ └── user.controller.ts
└── main.ts # Composition Root
Папка domain не импортирует ничего из infrastructure — это главный признак того, что правило зависимостей соблюдается.
Частые ошибки
Утечка зависимостей внутрь. Самая распространённая ошибка — импорт Prisma-моделей или Express-типов внутри Use Cases. Это нарушает правило зависимостей и делает бизнес-логику неотделимой от фреймворка.
Анемичные сущности. Когда User — просто объект с полями, а вся логика вынесена в сервисы, вы теряете главное преимущество: защиту инвариантов на уровне домена. Сущность должна сама знать, в каком состоянии она является корректной.
Избыточные абстракции. Создавать интерфейс для каждого класса ради следования паттерну — лишняя работа. Интерфейс нужен там, где есть реальная необходимость подменять реализацию: репозитории, почтовые сервисы, платёжные шлюзы.
Один сервис на всё. Не нужно создавать UserService с двадцатью методами. Один Use Case — одна операция. Это упрощает тестирование и делает код читаемым.
Игнорирование тестируемости. Главная практическая польза Clean Architecture — лёгкое модульное тестирование. Use Cases легко тестируются с мок-репозиториями без поднятия БД или HTTP-сервера. Если тесты не пишутся, половина ценности подхода теряется.
Заключение
Clean Architecture — это не жёсткий канон, а набор принципов, которые адаптируются под нужды проекта. Для небольших CRUD-приложений полная реализация со всеми слоями избыточна. Но для сложных доменов с развитой бизнес-логикой этот подход даёт ощутимые преимущества: независимость от фреймворков, простоту тестирования и долгосрочную поддерживаемость кода.
Начните с малого: выделите Use Cases из контроллеров и определите интерфейсы репозиториев. Этих двух шагов уже достаточно, чтобы почувствовать разницу.






Комментарии
0