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

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

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

Аналитика и логирование

shared/lib/
├── analytics/
│ ├── analytics.ts
│ └── events.ts
└── logger/
└── logger.ts
// shared/lib/analytics/analytics.ts
export const analytics = {
track: (event: string, properties?: Record<string, any>) => {
// Отправка события в аналитическую систему
}
};

// features/article-favorite/model/use-toggle-favorite.ts
import { useMutation, useQueryClient } 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 });
}
});
}

Провайдеры и инициализация

// 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}</>;
}

Таблица размещения Cross-Cutting Concerns

Тип функциональностиРазмещениеОбоснованиеПример
Инфраструктурный клиент (SDK)shared/lib/Не содержит бизнес-логики, переиспользуетсяshared/lib/analytics/analytics.ts
Провайдер/инициализацияapp/providers/Глобальная настройка приложенияapp/providers/analytics.tsx
Доменное событиеentities/ или features/Привязано к бизнес-контекстуanalytics.track('article_favorited') в features/article-favorite
Feature flags (клиент)shared/lib/feature-flags/Технический механизмshared/lib/feature-flags/client.ts
Feature flags (проверка)Там, где используетсяБлизко к месту примененияif (isFeatureEnabled('newEditor')) в features
Error tracking (Sentry SDK)shared/lib/error-tracking/Инфраструктурный клиентshared/lib/sentry/client.ts
Error boundariesapp/ или pages/Глобальные/локальные обработчикиapp/error-boundary.tsx
Авторизация (проверка токена)entities/user/ или features/auth/Доменная логика пользователяentities/user/lib/check-auth.ts
Авторизация (guards)Там, где используетсяБлизко к защищаемому роутуpages/*/lib/require-auth.ts
Логирование (logger)shared/lib/logger/Технический механизмshared/lib/logger/logger.ts
Логирование (вызовы)Там, где нужноПо месту использованияlogger.error('Failed to load') в features

Принцип размещения:

  1. Инфраструктура → shared/lib/: Если это SDK, клиент, или технический механизм
  2. Инициализация → app/: Если это глобальная настройка при старте приложения
  3. Использование → Локально: Вызовы размещаются там, где они нужны (features, entities, pages)

Когда НЕ использовать Cross-Cutting паттерн:

FORBIDDEN: Создавать "универсальный" слой для функциональности, используемой в одном месте - размещайте код локально.

// ❌ Плохо - создан "cross-cutting" слой для одного использования
// shared/lib/article-tracking/track-article-view.ts
export function trackArticleView(slug: string) {
analytics.track('article_viewed', { slug });
}

// ✅ Хорошо - вызов напрямую там, где нужен
// features/article-detail/model/use-article-detail.ts
useEffect(() => {
analytics.track('article_viewed', { slug });
}, [slug]);

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

См. также: Слои и архитектура - почему импорты между features запрещены.

Проблема: Как две фичи могут взаимодействовать?

❌ FORBIDDEN: Прямой импорт между features

// ❌ features/article-favorite/model/use-toggle-favorite.ts
import { showNotification } from '@/features/notifications'; // FORBIDDEN!

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

return useMutation({
onSuccess: () => {
showNotification(t('favorited')); // Нарушение изоляции
}
});
}

Как выбрать правильное решение?

Используйте эту схему для принятия решения:

✅ Решение 1: Композиция на более высоком уровне (Lifting Up)

// ✅ 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>
);
}

✅ Решение 2: Dependency Inversion через props

// ✅ features/article-favorite/ui/favorite-button.tsx
import { useTranslations } from 'next-intl';

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 { useTranslations } from 'next-intl';
import { FavoriteButton } from '@/features/article-favorite';
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)}
/>
);
}

Основные подходы:

  • Lifting Up (Композиция): Родительский слой (widget/page) комбинирует независимые модули
  • Dependency Inversion: Модуль принимает зависимость через props/контекст вместо прямого импорта

📌 Ключевые моменты:

  • FORBIDDEN: Прямой импорт между features
  • SHOULD: Используйте Lifting Up если parent-слой может координировать
  • SHOULD: Используйте Dependency Inversion через props/callbacks для переиспользуемых features
  • MUST: Композиция решается на уровне widgets/pages

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

См. также: Слои и архитектура - правила импортов между слоями.

Правило Public API

MUST: Каждый слайс имеет единую точку входа - публичный контракт (обычно index.ts).

❌ FORBIDDEN: Импорт внутренних файлов

// ❌ FORBIDDEN - импорт из внутренних файлов
import { ArticleCard } from '@/entities/article/ui/article-card';
import { useArticle } from '@/entities/article/model/use-article';

✅ MUST: Импорт через Public API

// ✅ MUST - импорт только из корня модуля
import { ArticleCard, useArticle } from '@/entities/article';

Визуальная схема Public API

Ключевое правило: Всегда импортируйте из корня модуля через index.ts, никогда не обходите Public API напрямую.

Пример 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

  1. Инкапсуляция: Внутренние детали скрыты от внешнего кода
  2. Гибкость: Можно изменить внутреннюю структуру без влияния на внешний код
  3. Контроль: Явно определяем, что является публичным контрактом
  4. Простота: Легко понять, что можно использовать снаружи

📌 Ключевые моменты:

  • MUST: Импортируйте только из корня модуля через index.ts
  • FORBIDDEN: Глубокие импорты (@/entities/article/ui/article-card)
  • MUST: Public API определяет контракт модуля
  • MUST: Внутренние файлы остаются приватными