Перейти к основному содержимому

Коммуникация

Цель: Обеспечить независимость модулей, избежать прямых зависимостей между features и сохранить предсказуемую архитектуру.


Сквозные задачи (Cross-Cutting Concerns)

MUST

  • Инфраструктурные клиенты (SDK) размещать в shared/lib/.
// ✅ shared/lib/analytics/analytics.ts
export const analytics = {
track: (event: string, properties?: Record<string, any>) => {
// Отправка события в аналитическую систему
},
init: (apiKey: string) => {
// Инициализация SDK
},
};
  • Глобальную инициализацию и провайдеры размещать в app/providers/.
// ✅ app/providers/analytics.tsx
import { useEffect, type PropsWithChildren } from 'react';
import { analytics } from '@/shared/lib/analytics';
import { config } from '@/shared/config';

type Props = PropsWithChildren;

export function AnalyticsProvider({ children }: Props) {
useEffect(() => {
analytics.init(config.analyticsId);
}, []);

return <>{children}</>;
}
  • Вызовы сквозной функциональности размещать локально в месте использования.
// ✅ features/article-favorite/model/use-toggle-favorite.ts
import { useMutation } from '@tanstack/react-query';
import { analytics } from '@/shared/lib/analytics';

export function useToggleFavorite(slug: string) {
return useMutation({
mutationFn: toggleFavorite,
onSuccess: () => {
analytics.track('article_favorited', { slug });
},
});
}
  • Следовать таблице размещения Cross-Cutting Concerns:
Тип функциональностиРазмещениеОбоснование
Инфраструктурный клиент (Analytics SDK, Logger SDK)shared/lib/Не содержит бизнес-логики, переиспользуется
Провайдер/инициализацияapp/providers/Глобальная настройка приложения
Доменное событиеentities/ или features/Привязано к бизнес-контексту
Feature flags (клиент)shared/lib/feature-flags/Технический механизм
Feature flags (проверка)Там, где используетсяБлизко к месту применения
Error tracking (Sentry SDK)shared/lib/error-tracking/Инфраструктурный клиент
Error boundariesapp/ или pages/Глобальные/локальные обработчики
Авторизация (проверка токена)entities/user/ или features/auth/Доменная логика пользователя
Авторизация (guards)Там, где используетсяБлизко к защищаемому роуту
Логирование (logger)shared/lib/logger/Технический механизм
Логирование (вызовы)Там, где нужноПо месту использования

SHOULD

  • Структурировать shared/lib/ по назначению функциональности.
shared/lib/
├── analytics/
│ ├── analytics.ts
│ └── events.ts
├── logger/
│ └── logger.ts
├── feature-flags/
│ └── client.ts
└── error-tracking/
└── sentry.ts
  • Использовать типизацию для событий аналитики.
// shared/lib/analytics/events.ts
export type AnalyticsEvent =
| { type: 'article_favorited'; slug: string }
| { type: 'user_registered'; email: string }
| { type: 'payment_completed'; amount: number };

// shared/lib/analytics/analytics.ts
export const analytics = {
track: (event: AnalyticsEvent) => {
// Типобезопасная отправка событий
},
};

FORBIDDEN

  • Создавать "универсальный" слой для функциональности, используемой в одном месте.
// ❌ FORBIDDEN - создан "cross-cutting" слой для одного использования
// shared/lib/article-tracking/track-article-view.ts
export function trackArticleView(slug: string) {
analytics.track('article_viewed', { slug });
}

// ✅ CORRECT - вызов напрямую там, где нужен
// features/article-detail/model/use-article-detail.ts
useEffect(() => {
analytics.track({ type: 'article_viewed', slug });
}, [slug]);
  • Размещать бизнес-логику в shared/lib/.
// ❌ FORBIDDEN - бизнес-логика в shared
// shared/lib/article-validation/validate-article.ts
export function validateArticle(article: Article): boolean {
return article.title.length > 10 && article.content.length > 100;
}

// ✅ CORRECT - бизнес-логика в entities
// entities/article/lib/validate-article.ts
export function validateArticle(article: Article): boolean {
return article.title.length > 10 && article.content.length > 100;
}

Коммуникация между фичами (Cross-Feature Communication)

Визуализация проблемы

MUST

  • Использовать композицию на уровне widgets/ или pages/ для координации независимых features.
// ✅ widgets/article-detail/ui/article-detail.tsx
import { useTranslations } from 'next-intl';
import { FavoriteButton } from '@/features/article-favorite';
import { showNotification } from '@/features/notifications';

export function ArticleDetail({ article }: Props) {
const t = useTranslations('article');

const handleFavoriteSuccess = () => {
showNotification(t('favorited'));
};

return (
<div>
<ArticleCard article={article} />
<FavoriteButton
article={article}
onSuccess={handleFavoriteSuccess}
/>
</div>
);
}
  • Применять Dependency Inversion через props/callbacks для переиспользуемых features.
// ✅ features/article-favorite/ui/favorite-button.tsx
interface FavoriteButtonProps {
article: Article;
onSuccess?: () => void; // Инверсия зависимости
onError?: (error: Error) => void;
}

export function FavoriteButton({ article, onSuccess, onError }: Props) {
const { mutate } = useToggleFavorite({
onSuccess,
onError
});
const t = useTranslations('common');

return (
<button onClick={() => mutate(article)}>
{t('actions.favorite')}
</button>
);
}

// pages/article-details/ui/article-details-page.tsx
import { toast } from '@/shared/ui/toast';

export function ArticleDetailsPage() {
const t = useTranslations('article');

return (
<FavoriteButton
article={article}
onSuccess={() => toast.success(t('favorited'))}
onError={(e) => toast.error(e.message)}
/>
);
}

SHOULD

  • Предпочитать Lifting Up (композицию) как основной паттерн, если родительский слой может координировать взаимодействие.
  • Использовать Dependency Inversion когда feature должна быть переиспользуемой с различным поведением.

FORBIDDEN

  • Прямой импорт между модулями слоя features/.
// ❌ FORBIDDEN
// features/article-favorite/model/use-toggle-favorite.ts
import { showNotification } from '@/features/notifications'; // ЗАПРЕЩЕНО!

export function useToggleFavorite() {
return useMutation({
onSuccess: () => {
showNotification('Favorited!'); // Нарушение изоляции
},
});
}
  • Создавать "посредников" или "медиаторы" между features в shared/.
// ❌ FORBIDDEN - медиатор в shared
// shared/lib/feature-mediator/mediator.ts
export const featureMediator = {
notifyFavorite: (article: Article) => {
showNotification('Favorited!');
},
};

Public API и изоляция модулей

Визуализация Public API

MUST

  • Каждый модуль имеет единую точку входа — файл index.ts (Public API).
// ✅ entities/article/index.ts
// Public API модуля article

// UI Components
export { ArticleCard } from './ui/article-card';
export { ArticleMeta } from './ui/article-meta';
export { ArticleList } from './ui/article-list';

// Model
export { useArticle, useArticles } from './model/use-article';
export type { Article, ArticleFormData } from './model/article.types';

// Lib (только публичные утилиты)
export { formatArticleDate } from './lib/format-date';
export { slugify } from './lib/slugify';

// API НЕ экспортируется - это внутренняя деталь реализации
  • Импортировать модули только через Public API (из корня модуля).
// ✅ CORRECT - импорт через Public API
import { ArticleCard, useArticle } from '@/entities/article';

// Использование
export function ArticlePage() {
const { data: article } = useArticle(slug);

return <ArticleCard article={article} />;
}
  • Экспортировать только публичные элементы модуля, скрывать детали реализации.
// entities/article/index.ts

// ✅ Экспортируем публичный API
export { ArticleCard } from './ui/article-card';
export { useArticle } from './model/use-article';

// ❌ НЕ экспортируем внутренние детали
// export { articleApi } from './api/article-api'; // Приватно
// export { ArticleCardSkeleton } from './ui/article-card-skeleton'; // Приватно

SHOULD

  • Использовать явные экспорты вместо wildcard-экспортов (export *).
// ✅ PREFERRED - явные экспорты
export { ArticleCard } from './ui/article-card';
export { ArticleMeta } from './ui/article-meta';

// ⚠️ AVOID - wildcard может ухудшить tree-shaking
export * from './ui';
  • Для динамически загружаемых модулей создавать отдельные entry points.
// entities/article/index.ts - основной Public API
export { ArticleCard } from './ui/article-card';

// entities/article/modal.ts - отдельный entry point для lazy loading
export { ArticleModal } from './ui/article-modal';

// Использование
const ArticleModal = lazy(() => import('@/entities/article/modal'));

MAY

  • В простых проектах можно не создавать index.ts, но правила импорта остаются:
    • Импортировать только из корня модуля
    • Не использовать глубокие импорты из поддиректорий
// MAY - простой проект без index.ts
// features/article-favorite/ui/favorite-button.tsx

// ✅ Допустимо в простых проектах
import { FavoriteButton } from '@/features/article-favorite/ui/favorite-button';

// ❌ Всё равно запрещено
import { toggleFavorite } from '@/features/article-favorite/api/toggle-favorite';

FORBIDDEN

  • Импорт из внутренних файлов и поддиректорий модуля (глубокие импорты).
// ❌ FORBIDDEN - глубокие импорты
import { ArticleCard } from '@/entities/article/ui/article-card';
import { useArticle } from '@/entities/article/model/use-article';
import { articleApi } from '@/entities/article/api/article-api';

// ✅ CORRECT - импорт через Public API
import { ArticleCard, useArticle } from '@/entities/article';
  • Создавать "глобальные" barrel-файлы, объединяющие несвязанные элементы.
// ❌ FORBIDDEN - один index.ts для всего shared/ui
// shared/ui/index.ts
export * from './button';
export * from './input';
export * from './modal';
export * from './table';
// ... 50+ компонентов

// ✅ CORRECT - каждый компонент имеет свой Public API
// shared/ui/button/index.ts
export { Button } from './button';
export type { ButtonProps } from './button';
  • Экспортировать детали реализации через Public API.
// ❌ FORBIDDEN
// entities/article/index.ts
export { articleApi } from './api/article-api'; // Внутренняя деталь
export { ArticleCardSkeleton } from './ui/article-card-skeleton'; // Приватный компонент

// ✅ CORRECT
// Эти элементы остаются приватными внутри модуля

Преимущества правильной коммуникации

  1. Изоляция модулей:
    Внутренние изменения не влияют на внешний код

  2. Гибкость:
    Можно изменить внутреннюю структуру без рефакторинга импортов

  3. Контроль зависимостей:
    Явно определяем публичный контракт модуля

  4. Масштабируемость:
    Модули можно развивать независимо друг от друга

  5. Предсказуемость:
    Легко понять, какие элементы доступны снаружи