← Назад к вопросам

Что такое принцип чистой архитектуры?

2.4 Senior🔥 191 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Clean Architecture: чистая архитектура

Определение

Clean Architecture это методология проектирования, разработанная Робертом Мартином (Uncle Bob). Цель: создавать системы которые:

  • Независимы от фреймворков
  • Тестируемы
  • Независимы от UI
  • Независимы от БД
  • Независимы от любых external agency

Слои архитектуры

┌──────────────────────────────────┐
│     Frameworks & Drivers         │ (Web, DB, UI)
├──────────────────────────────────┤
│     Interface Adapters           │ (Controllers, Presenters)
├──────────────────────────────────┤
│     Application Business Rules   │ (Use Cases)
├──────────────────────────────────┤
│     Enterprise Business Rules    │ (Entities, Domain)
└──────────────────────────────────┘

4 Слоя

1. Entities (Сущности) Бизнес-правила уровня предприятия. Независимы от приложения.

class User {
  id: number;
  email: string;
  password: string; // хэшированный
  
  isValidPassword(plain: string): boolean {
    return bcrypt.compareSync(plain, this.password);
  }
}

2. Use Cases (Сценарии использования) Бизнес-правила приложения. Описывают что может делать приложение.

class LoginUseCase {
  execute(email: string, password: string) {
    const user = this.userRepo.findByEmail(email);
    if (!user) throw new Error('User not found');
    if (!user.isValidPassword(password)) throw new Error('Invalid password');
    return { userId: user.id };
  }
}

3. Interface Adapters (Адаптеры интерфейсов) Конвертируют между слоями. Controllers, Presenters, Gateways.

@Controller('/auth')
class LoginController {
  @Post('/login')
  async login(@Body() dto: LoginDto) {
    const result = await this.loginUseCase.execute(dto.email, dto.password);
    return { token: this.jwtService.sign(result) };
  }
}

4. Frameworks & Drivers (Фреймворки и драйверы) Веб, БД, UI. Детали реализации.

// Express, NestJS, Database drivers
const app = express();
app.post('/auth/login', loginController.handle);

Правило зависимостей (Dependency Rule)

Все зависимости должны указывать внутрь:

Outer layers CAN depend on inner layers
Inner layers CANNOT depend on outer layers

✅ Controllers → Use Cases → Entities
❌ Entities → Controllers (запрещено!)

Структура проекта

src/
├── domain/              # Entities, Business Rules
│   ├── entities/
│   │   └── User.ts
│   └── interfaces/
│       └── UserRepository.ts
│
├── application/         # Use Cases
│   ├── usecases/
│   │   └── LoginUseCase.ts
│   └── dtos/
│       └── LoginDto.ts
│
├── infrastructure/      # Adapters, External Services
│   ├── repositories/
│   │   └── UserRepository.ts (реализация)
│   └── services/
│       └── JwtService.ts
│
└── presentation/        # Controllers, Views
    ├── controllers/
    │   └── LoginController.ts
    └── routes/
        └── auth.routes.ts

DI Container

// Инверсия управления (IoC)
const loginUseCase = new LoginUseCase(
  userRepository, // Dependency Injection
  passwordHasher
);

const loginController = new LoginController(
  loginUseCase
);

Пример: Полный Use Case

// domain/entities/User.ts
class User {
  constructor(id: number, email: string, passwordHash: string) {
    this.id = id;
    this.email = email;
    this.passwordHash = passwordHash;
  }
  
  isValidPassword(plain: string): boolean {
    return bcrypt.compareSync(plain, this.passwordHash);
  }
}

// domain/interfaces/UserRepository.ts
interface IUserRepository {
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

// application/usecases/LoginUseCase.ts
class LoginUseCase {
  constructor(private userRepository: IUserRepository) {}
  
  async execute(email: string, password: string) {
    const user = await this.userRepository.findByEmail(email);
    if (!user) throw new Error('User not found');
    if (!user.isValidPassword(password)) throw new Error('Invalid credentials');
    return { userId: user.id };
  }
}

// infrastructure/repositories/UserRepository.ts
class UserRepository implements IUserRepository {
  constructor(private db: Database) {}
  
  async findByEmail(email: string): Promise<User | null> {
    const record = await this.db.query('SELECT * FROM users WHERE email = ?', [email]);
    return record ? new User(record.id, record.email, record.password_hash) : null;
  }
  
  async save(user: User): Promise<void> {
    await this.db.query('INSERT INTO users VALUES (?, ?, ?)', [user.id, user.email, user.passwordHash]);
  }
}

// presentation/controllers/LoginController.ts
@Controller('/auth')
class LoginController {
  constructor(private loginUseCase: LoginUseCase) {}
  
  @Post('/login')
  async handle(@Body() dto: { email: string; password: string }) {
    const result = await this.loginUseCase.execute(dto.email, dto.password);
    return { token: jwt.sign({ userId: result.userId }) };
  }
}

Тестирование в Clean Architecture

// Легко тестировать Use Cases
class MockUserRepository implements IUserRepository {
  async findByEmail(email: string) {
    return new User(1, email, 'hashed_password');
  }
}

test('LoginUseCase should return userId', async () => {
  const mockRepo = new MockUserRepository();
  const useCase = new LoginUseCase(mockRepo);
  
  const result = await useCase.execute('test@example.com', 'password');
  expect(result.userId).toBe(1);
});

Плюсы

✅ Независимость от фреймворков
✅ Тестируемость
✅ Простота изменения деталей
✅ Четкое разделение ответственности
✅ Переиспользуемый бизнес-код

Минусы

❌ Более сложный setup
❌ Больше файлов и классов
❌ Learning curve
❌ Может быть over-engineering для маленьких проектов

Когда использовать

✅ Для больших проектов
✅ Долгосрочная разработка
✅ Команда разработчиков
✅ Нужна высокая testability

❌ Маленькие скрипты
❌ MVP и prototypes
❌ Один разработчик, простой проект
Что такое принцип чистой архитектуры? | PrepBro