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

Как бы изменил архитектуру проекта и распределение ответственности с текущим опытом?

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

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

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

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

Эволюция архитектуры Frontend проекта

Применяя 10+ лет опыта, я бы значительно переработал архитектуру типичного frontend проекта. Это касается разделения ответственности, управления состоянием, организации кода и подходов к масштабируемости.

Текущие проблемы в стандартных проектах

Большинство frontend проектов страдают от:

  1. Размешивание логики - business logic в компонентах
  2. Отсутствие слоёв - нет чёткого разделения ответственности
  3. Монолитные компоненты - сложно переиспользовать и тестировать
  4. Глобальное состояние везде - Redux/Context для всего подряд
  5. Отсутствие типизации - any вместо proper types
  6. Плохая организация - компоненты, utils, hooks в одной папке

Предлагаемая архитектура: Clean Architecture

Я использовал бы Clean Architecture с чёткими слоями:

src/
|
+-- domain/
|   +-- entities/          # Бизнес-объекты (User, Post, etc)
|   +-- interfaces/        # Contracts (IUserRepository, IAuthService)
|   +-- value-objects/     # Immutable objects (Email, UserId)
|   +-- use-cases/         # Бизнес-логика
|       +-- GetUser/
|       +-- CreateComment/
|       +-- SearchQuestions/
|
+-- application/
|   +-- dto/               # Data Transfer Objects
|   +-- mappers/           # Entity <-> DTO
|   +-- services/          # Orchestration layer
|   +-- queries/           # Query handlers (CQRS pattern)
|   +-- commands/          # Command handlers (CQRS pattern)
|
+-- infrastructure/
|   +-- api/               # HTTP client, API calls
|   +-- storage/           # LocalStorage, SessionStorage
|   +-- repositories/      # Implementation of domain interfaces
|   +-- services/          # 3rd party integrations
|
+-- presentation/
|   +-- components/        # React components
|       +-- common/        # Reusable UI components
|       +-- features/      # Feature-specific components
|   +-- hooks/             # Custom React hooks
|   +-- pages/             # Page components
|   +-- contexts/          # React Context for UI state only
|   +-- styles/            # Global styles
|
+-- shared/
    +-- utils/             # Pure utility functions
    +-- constants/         # App-wide constants
    +-- types/             # Global TypeScript types

Слой Domain - Центр архитектуры

Domain layer содержит pure business logic без зависимостей от фреймворков:

// domain/entities/User.ts
export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

// domain/value-objects/Email.ts
export class Email {
  private value: string;

  constructor(email: string) {
    if (!this.isValid(email)) {
      throw new Error('Invalid email');
    }
    this.value = email;
  }

  private isValid(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  getValue(): string {
    return this.value;
  }
}

// domain/interfaces/IUserRepository.ts
export interface IUserRepository {
  getById(id: string): Promise<User | null>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

// domain/use-cases/GetUser.ts
export class GetUserUseCase {
  constructor(private userRepository: IUserRepository) {}

  async execute(userId: string): Promise<User | null> {
    return this.userRepository.getById(userId);
  }
}

Слой Application - Оркестрация

Application layer оркестрирует domain logic и infrastructure:

// application/dto/UserDTO.ts
export interface UserDTO {
  id: string;
  email: string;
  name: string;
  createdAt: string; // ISO format
}

// application/mappers/UserMapper.ts
import { User } from '@domain/entities/User';

export class UserMapper {
  static toDomain(dto: UserDTO): User {
    return {
      id: dto.id,
      email: dto.email,
      name: dto.name,
      createdAt: new Date(dto.createdAt)
    };
  }

  static toDTO(user: User): UserDTO {
    return {
      id: user.id,
      email: user.email,
      name: user.name,
      createdAt: user.createdAt.toISOString()
    };
  }
}

// application/services/UserApplicationService.ts
export class UserApplicationService {
  constructor(
    private getUserUseCase: GetUserUseCase,
    private userMapper: UserMapper
  ) {}

  async getUser(userId: string): Promise<UserDTO> {
    const user = await this.getUserUseCase.execute(userId);
    if (!user) throw new Error('User not found');
    return this.userMapper.toDTO(user);
  }
}

Слой Infrastructure - Реализация

Infrastructure layer реализует interfaces и работает с внешним миром:

// infrastructure/repositories/UserRepository.ts
import { User } from '@domain/entities/User';
import { IUserRepository } from '@domain/interfaces/IUserRepository';

export class UserRepository implements IUserRepository {
  constructor(private apiClient: ApiClient) {}

  async getById(id: string): Promise<User | null> {
    try {
      const response = await this.apiClient.get(`/api/users/${id}`);
      return {
        id: response.id,
        email: response.email,
        name: response.name,
        createdAt: new Date(response.created_at)
      };
    } catch (error) {
      return null;
    }
  }

  async save(user: User): Promise<void> {
    await this.apiClient.post('/api/users', {
      id: user.id,
      email: user.email,
      name: user.name,
      created_at: user.createdAt.toISOString()
    });
  }

  async delete(id: string): Promise<void> {
    await this.apiClient.delete(`/api/users/${id}`);
  }
}

Слой Presentation - React компоненты

Presentation layer - тонкие компоненты, только отрисовка:

// presentation/hooks/useGetUser.ts
import { useState, useEffect } from 'react';
import { useUserService } from '@presentation/contexts/ServiceContext';
import { UserDTO } from '@application/dto/UserDTO';

export function useGetUser(userId: string) {
  const [data, setData] = useState<UserDTO | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const userService = useUserService();

  useEffect(() => {
    userService.getUser(userId)
      .then(user => setData(user))
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, [userId, userService]);

  return { data, loading, error };
}

// presentation/components/features/UserProfile.tsx
interface UserProfileProps {
  userId: string;
}

export function UserProfile({ userId }: UserProfileProps) {
  const { data: user, loading, error } = useGetUser(userId);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>Not found</div>;

  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{new Date(user.createdAt).toLocaleDateString()}</p>
    </div>
  );
}

Управление состоянием

Использовать состояние стратегически:

// Local state - для UI состояния (открыт ли modal)
const [isModalOpen, setIsModalOpen] = useState(false);

// Custom hooks - для бизнес-логики
function useUserSearch(query: string) {
  const [results, setResults] = useState([]);
  const userService = useUserService();

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    userService.search(query).then(setResults);
  }, [query, userService]);

  return results;
}

// Context - ТОЛЬКО для глобального UI состояния (theme, auth)
const AuthContext = createContext<AuthContextValue | null>(null);

// React Query / SWR - для кеширования данных
const { data: users } = useQuery({
  queryKey: ['users'],
  queryFn: () => userService.getAll()
});

Dependency Injection

Инъектировать зависимости вместо импорта:

// shared/container/Container.ts
class Container {
  private services = new Map();

  register<T>(key: string, factory: () => T): void {
    this.services.set(key, factory);
  }

  get<T>(key: string): T {
    const factory = this.services.get(key);
    if (!factory) throw new Error(`Service ${key} not found`);
    return factory();
  }
}

// root/bootstrap.ts
const container = new Container();

const apiClient = new ApiClient(BASE_URL);
container.register('apiClient', () => apiClient);

const userRepository = new UserRepository(apiClient);
container.register('userRepository', () => userRepository);

const getUserUseCase = new GetUserUseCase(userRepository);
container.register('getUserUseCase', () => getUserUseCase);

const userService = new UserApplicationService(getUserUseCase, UserMapper);
container.register('userService', () => userService);

export default container;

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

С чёткими слоями тестирование становится простым:

// __tests__/domain/use-cases/GetUser.test.ts
describe('GetUserUseCase', () => {
  it('should return user by id', async () => {
    const mockRepository: IUserRepository = {
      getById: jest.fn().mockResolvedValue({
        id: '1',
        email: 'test@example.com',
        name: 'Test User',
        createdAt: new Date()
      }),
      save: jest.fn(),
      delete: jest.fn()
    };

    const useCase = new GetUserUseCase(mockRepository);
    const user = await useCase.execute('1');

    expect(user).toBeDefined();
    expect(user.email).toBe('test@example.com');
    expect(mockRepository.getById).toHaveBeenCalledWith('1');
  });
});

Версионирование API

Сделать проект готовым к изменениям API:

// infrastructure/api/v1/UserAPI.ts
export class UserAPIv1 implements IUserRepository {
  async getById(id: string): Promise<User | null> {
    const response = await fetch(`/api/v1/users/${id}`);
    return response.json();
  }
}

// infrastructure/api/v2/UserAPI.ts
export class UserAPIv2 implements IUserRepository {
  async getById(id: string): Promise<User | null> {
    const response = await fetch(`/api/v2/users/${id}`);
    // Маппинг нового формата к старому
    return this.mapResponse(response);
  }

  private mapResponse(data: any): User {
    // Логика маппинга между версиями
    return {...};
  }
}

Ключевые принципы

  1. Dependency Rule - зависимости только во внутрь (presentation -> application -> domain)
  2. Interface Segregation - используй interfaces для абстракций
  3. DRY & KISS - не повторяй код, не усложняй
  4. SOLID - применяй ко всем слоям
  5. Type Safety - TypeScript strict mode везде
  6. Testability - каждый слой тестируется независимо

Современные инструменты

  • Next.js - встроенная поддержка API routes
  • React Query / SWR - управление асинхронным состоянием
  • TypeScript - type safety
  • Jest + Testing Library - тестирование
  • Turborepo / Monorepo - управление большими проектами
  • Zod / Yup - валидация данных

Итог

С опытом я бы внедрил:

  1. Clean Architecture с чёткими слоями
  2. Domain-Driven Design для сложной бизнес-логики
  3. Dependency Injection для гибкости и тестируемости
  4. Use Cases вместо случайной логики в компонентах
  5. Proper State Management - React Query для данных, Context для UI
  6. Strong Typing - TypeScript strict, no any
  7. Monorepo структура для масштабируемости
  8. SOLID принципы везде

Это требует больше инициального кода, но делает проект:

  • Тестируемым
  • Масштабируемым
  • Поддерживаемым
  • Легким для новых разработчиков
  • Готовым к изменениям требований