📌 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 — сборка графа зависимостей

Если забыть про эти вещи, то чистая архитектура быстро превратиться в большое количество директорий.