📌 Single Responsibility Principle (S)
Один use case === один сценарий поведения системы. Проще говоря: каждый класс должен отвечать только за одну причину изменения.
😭 Плохо:
class UserUseCase {
register() {}
login() {}
resetPassword() {}
}
Этот класс наверняка будет изменяться по разным причинам: правки могут прилететь в разные сценарии.
👍 Лучше:
RegisterUserUseCase
LoginUserUseCase
ResetPasswordUseCase
Каждый use case отвечает за определённый сценарий. В репозитории есть примеры RegisterUserUseCase и LoginUserUseCase.
📌 Open/Closed Principle (O)
В контексте чистой архитектуры этот принцип особенно важен Напомню, принцип говорит нам, что класс должен быть открыт для расширения, но закрыт для изменения. Смотрим сразу на примере демонстрационного репозитория. У меня есть реализация ScryptPasswordHasher. Представим, что потребовалось реализовать хеширование через другую библиотеку. Если использовать в сервисе хешер напрямую:
class UserService {
private hasher = new ScryptPasswordHasher()
}
То придётся обновлять сам сервис. А если отталкиваемся от интерфейса:
class UserService {
constructor(private readonly hasher: PasswordHasher) {}
}
то мы просто подменим реализацию на уровне инфраструктуры или DI-контейнера. Бизнес-логика при этом не меняется. Мы расширяем систему новой реализацией, но существующий код не изменяем.
📌 Dependency Inversion Principle (D)
Наглядный пример в коде — зависим от абстракции, а не от SQLite.
constructor(private readonly userRepository: UserRepository)
📌 Паттерны тоже есть
На самом деле в этом простом проекте не обошлось и без паттернов. Например:
- Strategy — хеширование библиотекой
- Factory Method — пример в UserResponse.fromEntity()
- Dependency Injection — сборка графа зависимостей
Если забыть про эти вещи, то чистая архитектура быстро превратиться в большое количество директорий.