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

Практические примеры решений

Цель: Помочь принимать решения о размещении кода через практические примеры.


Пример 1: Добавление лайка к статье

Вопрос: Где разместить логику добавления/удаления лайка?

MUST

  • Пользовательские сценарии размещать в features/.
// ✅ CORRECT - пользовательский сценарий в features/
// features/article-favorite/model/use-toggle-favorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invalidateArticleListRelatedQueries } from '@/entities/article';
import {
favoriteArticle,
unfavoriteArticle,
} from '@/shared/api/endpoints/article.api';

export function useToggleFavorite(slug: string) {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (isFavorited: boolean) =>
isFavorited ? unfavoriteArticle(slug) : favoriteArticle(slug),
onSuccess: () => {
invalidateArticleListRelatedQueries(queryClient);
},
});
}
  • UI компоненты сценария размещать в той же feature.
// ✅ features/article-favorite/ui/favorite-button.tsx
import { useTranslations } from 'next-intl';
import { useToggleFavorite } from '../model/use-toggle-favorite';
import type { Article } from '@/entities/article';

interface FavoriteButtonProps {
record: Article;
}

export function FavoriteButton({ record }: FavoriteButtonProps) {
const { mutate } = useToggleFavorite(record.slug);
const t = useTranslations('common');

const handleToggleFavorite = () => {
mutate(record.favorited);
};

return (
<button onClick={handleToggleFavorite}>
{t('actions.favor', { isFavorite: record.favorited })}
<span>{record.favoritesCount}</span>
</button>
);
}

Обоснование размещения в features/

Почему features/article-favorite/?

  1. Это законченный пользовательский сценарий
  2. Управляет состоянием и обработкой ошибок
  3. Оркеструет взаимодействие с entities и API
  4. Может быть переиспользован на разных страницах

FORBIDDEN

  • Размещать пользовательские сценарии в entities/.
// ❌ FORBIDDEN - сценарий в entities
// entities/article/model/use-toggle-favorite.ts
export function useToggleFavorite(slug: string) {
// Это сценарий, а не бизнес-логика сущности!
}
  • Размещать сценарии в shared/.
// ❌ FORBIDDEN - сценарий в shared
// shared/hooks/use-toggle-favorite.ts
export function useToggleFavorite(slug: string) {
// shared не содержит бизнес-логику!
}

Пример 2: Валидация email

Вопрос: Где разместить валидацию email?

MUST

  • Технические валидаторы без бизнес-логики размещать в shared/lib/validators/.
// ✅ CORRECT - техническая утилита в shared
// shared/lib/validators/email.ts
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
  • Использовать технические валидаторы через импорт в features.
// ✅ CORRECT - использование в features
// features/auth/model/validation.ts
import { isValidEmail } from '@/shared/lib/validators/email';
import { z } from 'zod';

export const createLoginSchema = (t: TFunction) =>
z.object({
email: z.string().refine(isValidEmail, t('errors.invalidEmailFormat')),
password: z.string().min(8, t('errors.passwordTooShort')),
});

SHOULD

  • Бизнес-специфичные валидации размещать в entities/.
// ✅ SHOULD - валидация с бизнес-правилами в entities
// entities/user/lib/validate-corporate-email.ts
export function isValidCorporateEmail(email: string): boolean {
// Бизнес-правило: только корпоративные домены
const corporateDomains = ['company.com', 'enterprise.org'];
const domain = email.split('@')[1];
return corporateDomains.includes(domain);
}

// Использование
// features/auth/model/validation.ts
import { isValidCorporateEmail } from '@/entities/user';

export const createCorporateLoginSchema = (t: TFunction) =>
z.object({
email: z
.string()
.refine(isValidCorporateEmail, t('errors.notCorporateEmail')),
});

FORBIDDEN

  • Размещать техническую валидацию в entities/ если она не содержит доменных правил.
// ❌ FORBIDDEN - техническая валидация в entities
// entities/user/lib/validate-email.ts
export function validateEmail(email: string) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// ✅ CORRECT - в shared
// shared/lib/validators/email.ts
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

Пример 3: Форматирование даты

Вопрос: Где разместить функцию форматирования даты?

MUST

  • Универсальные форматтеры размещать в shared/lib/formatters/.
// ✅ CORRECT - универсальный форматтер
// shared/lib/formatters/date.ts
import { format } from 'date-fns';

export function formatDate(date: Date | string, pattern = 'PPP'): string {
return format(new Date(date), pattern);
}

export function formatRelativeTime(date: Date | string): string {
// "2 hours ago", "3 days ago"
// ...
}
  • Доменно-специфичные форматтеры размещать в соответствующей entities/.
// ✅ CORRECT - доменный форматтер
// entities/article/lib/format-date.ts
import { formatDate } from '@/shared/lib/formatters/date';

export function formatArticleDate(article: Article): string {
// Специфичная логика для статей: показываем относительное время
// для новых статей, абсолютную дату для старых
const daysOld = getDaysOld(article.createdAt);

if (daysOld < 7) {
return formatRelativeTime(article.createdAt);
}

return formatDate(article.createdAt, 'MMM d, yyyy');
}

SHOULD

  • Переиспользовать базовые форматтеры из shared/ в доменных форматтерах.
// ✅ SHOULD
// entities/article/lib/format-date.ts
import { formatDate, formatRelativeTime } from '@/shared/lib/formatters/date';

export function formatArticleDate(article: Article): string {
// Используем базовые форматтеры + доменная логика
return article.isRecent
? formatRelativeTime(article.createdAt)
: formatDate(article.createdAt, 'MMM d, yyyy');
}

FORBIDDEN

  • Дублировать базовую логику форматирования в entities.
// ❌ FORBIDDEN - дублирование базовой логики
// entities/article/lib/format-date.ts
import { format } from 'date-fns';

export function formatArticleDate(date: string): string {
// Базовая логика должна быть в shared/lib/formatters/
return format(new Date(date), 'PPP');
}

// ✅ CORRECT - используем shared
import { formatDate } from '@/shared/lib/formatters/date';

export function formatArticleDate(date: string): string {
return formatDate(date, 'PPP');
}

Пример 4: Загрузка и отображение списка статей

Вопрос: Где размещать получение данных, отображение и фильтрацию?

MUST

  • API-слой размещать в shared/api/endpoints/.
// ✅ shared/api/endpoints/article.api.ts
import { apiClient } from '@/shared/api/client';
import type { Article, ArticlesResponse } from '@/entities/article';

export async function getArticles(params: {
limit?: number;
offset?: number;
tag?: string;
}): Promise<ArticlesResponse> {
return apiClient.get('/articles', { params });
}
  • Хуки получения данных размещать в entities/*/model/.
// ✅ entities/article/model/use-articles.ts
import { useQuery } from '@tanstack/react-query';
import { getArticles } from '@/shared/api/endpoints/article.api';

export function useArticles(params: ArticlesParams) {
return useQuery({
queryKey: ['articles', params],
queryFn: () => getArticles(params),
});
}
  • UI компоненты сущностей размещать в entities/*/ui/.
// ✅ entities/article/ui/article-list.tsx
import type { Article } from '../model/article.types';
import { ArticleCard } from './article-card';

interface ArticleListProps {
articles: Article[];
isLoading?: boolean;
}

export function ArticleList({ articles, isLoading }: ArticleListProps) {
if (isLoading) return <ArticleListSkeleton />;

return (
<div>
{articles.map(article => (
<ArticleCard key={article.slug} article={article} />
))}
</div>
);
}
  • Логику фильтрации (пользовательский сценарий) размещать в features/.
// ✅ features/articles-filter/model/use-filter.ts
import { useState } from 'react';

export function useArticlesFilter() {
const [selectedTag, setSelectedTag] = useState<string>();
const [searchQuery, setSearchQuery] = useState('');

return {
selectedTag,
searchQuery,
setSelectedTag,
setSearchQuery,
clearFilters: () => {
setSelectedTag(undefined);
setSearchQuery('');
}
};
}

// ✅ features/articles-filter/ui/filter-form.tsx
export function ArticlesFilterForm({ onChange }: Props) {
const { selectedTag, setSelectedTag } = useArticlesFilter();

return (
<form>
<TagSelect value={selectedTag} onChange={setSelectedTag} />
</form>
);
}
  • Композицию размещать в widgets/ или pages/.
// ✅ widgets/articles-list/ui/articles-list.tsx
import { ArticleList } from '@/entities/article';
import { ArticlesFilterForm } from '@/features/articles-filter';

export function ArticlesListWidget() {
const { selectedTag, setSelectedTag } = useArticlesFilter();
const { data: articles } = useArticles({ tag: selectedTag });

return (
<div>
<ArticlesFilterForm
selectedTag={selectedTag}
onTagChange={setSelectedTag}
/>
<ArticleList articles={articles} />
</div>
);
}

FORBIDDEN

  • Размещать API-логику в entities.
// ❌ FORBIDDEN
// entities/article/api/article-api.ts
export const articleApi = {
getArticles: () => fetch('/api/articles'),
};

// ✅ CORRECT - API в shared
// shared/api/endpoints/article.api.ts
  • Размещать сценарии фильтрации в entities.
// ❌ FORBIDDEN - сценарий в entities
// entities/article/model/use-filter.ts
export function useArticlesFilter() {
// Фильтрация - это пользовательский сценарий!
}

// ✅ CORRECT
// features/articles-filter/model/use-filter.ts

Пример 5: Модальное окно подтверждения

Вопрос: Где разместить переиспользуемое модальное окно?

MUST

  • Универсальные UI-компоненты без бизнес-логики размещать в shared/ui/.
// ✅ shared/ui/modal/confirmation-modal.tsx
interface ConfirmationModalProps {
isOpen: boolean;
title: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
}

export function ConfirmationModal({
isOpen,
title,
message,
onConfirm,
onCancel
}: ConfirmationModalProps) {
return (
<Modal isOpen={isOpen} onClose={onCancel}>
<ModalHeader>{title}</ModalHeader>
<ModalBody>{message}</ModalBody>
<ModalFooter>
<Button onClick={onCancel} variant="secondary">Cancel</Button>
<Button onClick={onConfirm} variant="danger">Confirm</Button>
</ModalFooter>
</Modal>
);
}
  • Использовать generic компоненты через props в features.
// ✅ features/article-delete/ui/delete-article-button.tsx
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { ConfirmationModal } from '@/shared/ui/modal';
import { useDeleteArticle } from '../model/use-delete-article';

export function DeleteArticleButton({ article }: Props) {
const [isOpen, setIsOpen] = useState(false);
const { mutate } = useDeleteArticle();
const t = useTranslations('article');

const handleConfirm = () => {
mutate(article.slug);
setIsOpen(false);
};

return (
<>
<Button onClick={() => setIsOpen(true)} variant="danger">
{t('actions.delete')}
</Button>

<ConfirmationModal
isOpen={isOpen}
title={t('deleteConfirmation.title')}
message={t('deleteConfirmation.message', { title: article.title })}
onConfirm={handleConfirm}
onCancel={() => setIsOpen(false)}
/>
</>
);
}

SHOULD

  • Feature-специфичные модальные окна размещать в features/*/ui/.
// ✅ SHOULD - специфичная для feature логика
// features/article-share/ui/share-modal.tsx
import { Modal } from '@/shared/ui/modal';
import { useShareArticle } from '../model/use-share-article';

export function ShareArticleModal({ article, isOpen, onClose }: Props) {
const { shareToTwitter, shareToFacebook } = useShareArticle(article);

return (
<Modal isOpen={isOpen} onClose={onClose}>
<h2>Share "{article.title}"</h2>
<button onClick={shareToTwitter}>Twitter</button>
<button onClick={shareToFacebook}>Facebook</button>
</Modal>
);
}

FORBIDDEN

  • Размещать feature-специфичную логику в shared/ui/.
// ❌ FORBIDDEN - бизнес-логика в shared
// shared/ui/modal/delete-article-modal.tsx
export function DeleteArticleModal({ article }: Props) {
const { mutate } = useDeleteArticle(); // Бизнес-логика!

return <Modal>...</Modal>;
}

// ✅ CORRECT - бизнес-логика в features
// features/article-delete/ui/delete-article-modal.tsx

Сводная таблица принятия решений

Что размещаемКудаПочему
Пользовательский сценарий (лайк, удаление, фильтрация)features/Законченное действие пользователя
Техническая утилита (валидация email, форматирование)shared/lib/Переиспользуемая инфраструктура
Доменная логика (правила бизнеса)entities/Инварианты и правила сущности
API-клиентshared/api/Инфраструктурный слой
Хук получения данныхentities/*/model/Работа с данными сущности
UI компонент сущностиentities/*/ui/Отображение сущности
UI-kit компонентshared/ui/Переиспользуемый UI без логики
Композиция модулейwidgets/ или pages/Объединение независимых модулей
Специфичное модальное окноfeatures/*/ui/Часть пользовательского сценария
Универсальное модальное окноshared/ui/modal/Generic UI компонент