Clean Architecture принципы

Clean Architecture — это набор принципов организации кода, направленных на разделение ответственности, независимость бизнес-логики от фреймворков и упрощение поддержки проекта. Во фронтенде, и особенно во Vue.js, эти принципы позволяют избежать превращения компонентов в перегруженные «комбайны», где смешаны логика, состояние, запросы к API и представление.

Ключевая идея: бизнес-логика не должна зависеть от Vue, а Vue должен быть лишь способом отображения и связывания данных.


Основные слои архитектуры

Во Vue-проекте Clean Architecture обычно адаптируется под фронтенд-реалии, сохраняя фундаментальные слои.

Domain (доменный слой)

Содержит чистую бизнес-логику:

  • сущности (Entities)
  • value objects
  • интерфейсы репозиториев
  • доменные правила и ограничения

Этот слой:

  • не импортирует Vue, Pinia, Axios и другие библиотеки
  • не знает о браузере, HTTP или UI
// domain/user/User.js
export class User {
  constructor({ id, email, isActive }) {
    if (!email) {
      throw new Error('Email is required');
    }
    this.id = id;
    this.email = email;
    this.isActive = isActive;
  }

  deactivate() {
    this.isActive = false;
  }
}

Application (use cases)

Описывает сценарии использования приложения:

  • оркестрирует доменные сущности
  • вызывает репозитории
  • реализует бизнес-процессы

Use Case — это один законченный сценарий.

// application/users/DeactivateUser.js
export class DeactivateUser {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async execute(userId) {
    const user = await this.userRepository.getById(userId);
    user.deactivate();
    await this.userRepository.save(user);
    return user;
  }
}

Здесь отсутствует любая привязка к Vue или HTTP.


Infrastructure (инфраструктурный слой)

Реализует технические детали:

  • HTTP-запросы
  • работа с localStorage
  • реализация репозиториев
  • адаптеры API
// infrastructure/repositories/UserApiRepository.js
import axios from 'axios';
import { User } from '@/domain/user/User';

export class UserApiRepository {
  async getById(id) {
    const { data } = await axios.get(`/users/${id}`);
    return new User(data);
  }

  async save(user) {
    await axios.put(`/users/${user.id}`, user);
  }
}

Этот слой зависит от домена, но не наоборот.


Presentation (Vue-слой)

Содержит:

  • Vue-компоненты
  • composables
  • Pinia stores (если используются)
  • адаптацию use case под UI

Vue здесь выполняет роль «тонкого» слоя.

// presentation/composables/useDeactivateUser.js
import { DeactivateUser } from '@/application/users/DeactivateUser';
import { UserApiRepository } from '@/infrastructure/repositories/UserApiRepository';

export function useDeactivateUser() {
  const repository = new UserApiRepository();
  const useCase = new DeactivateUser(repository);

  const deactivate = async (userId) => {
    return await useCase.execute(userId);
  };

  return { deactivate };
}

Инверсия зависимостей

Ключевой принцип Clean Architecture — зависимости направлены внутрь.

  • Domain не знает ни о чём
  • Application зависит только от Domain
  • Infrastructure и Presentation зависят от Application и Domain

Используются интерфейсы и абстракции:

// domain/user/UserRepository.js
export class UserRepository {
  getById(id) {
    throw new Error('Not implemented');
  }

  save(user) {
    throw new Error('Not implemented');
  }
}

Use Case работает с этим интерфейсом, не зная, откуда приходят данные.


Роль Pinia в Clean Architecture

Pinia не является хранилищем бизнес-логики. В чистой архитектуре:

  • Pinia хранит состояние UI
  • не содержит сложных бизнес-правил
  • вызывает use case
// presentation/stores/userStore.js
import { defineStore } from 'pinia';
import { useDeactivateUser } from '@/presentation/composables/useDeactivateUser';

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
  }),

  actions: {
    async deactivateUser() {
      const { deactivate } = useDeactivateUser();
      this.user = await deactivate(this.user.id);
    },
  },
});

Компоненты Vue как слой адаптации

Vue-компоненты:

  • не знают о репозиториях
  • не содержат бизнес-логики
  • работают с состоянием и событиями
<script setup>
import { useUserStore } from '@/presentation/stores/userStore';

const store = useUserStore();
</script>

<template>
  <button @click="store.deactivateUser">
    Deactivate
  </button>
</template>

Компонент отвечает только за отображение и пользовательские действия.


Организация структуры проекта

Типичная структура Vue-проекта с Clean Architecture:

src/
├── domain/
│   └── user/
├── application/
│   └── users/
├── infrastructure/
│   └── repositories/
├── presentation/
│   ├── components/
│   ├── composables/
│   └── stores/

Такая структура:

  • масштабируется без переписывания
  • облегчает тестирование
  • снижает связность модулей

Тестируемость и изоляция

Use Case тестируется без Vue и браузера:

it('deactivates user', async () => {
  const fakeRepo = {
    getById: async () => new User({ id: 1, email: 'a@a.com', isActive: true }),
    save: async () => {},
  };

  const useCase = new DeactivateUser(fakeRepo);
  const user = await useCase.execute(1);

  expect(user.isActive).toBe(false);
});

Бизнес-логика проверяется изолированно, без моков UI и HTTP.


Практические ограничения и компромиссы

Во Vue-проектах Clean Architecture:

  • не должна применяться механически
  • оправдана в средних и крупных приложениях
  • может быть упрощена для небольших проектов

Главное — разделение ответственности, а не слепое следование шаблонам.