Files

12 KiB
Raw Permalink Blame History

title, description
title description
Модули Структура модуля, типы (UI, бизнес, инфра), публичный API, отличие модуля от компонента

Модули

Раздел описывает модуль как границу ответственности в SLM: что считается модулем, что такое компонент внутри модуля и как модуль взаимодействует с остальным кодом.

Определение

Модуль — минимальная архитектурная единица SLM. Он живёт на одном из слоёв, владеет конкретной областью ответственности и предоставляет наружу только публичный API.

Модуль может содержать всё, что нужно этой области: компоненты, вложенные модули, хуки, сторы, сервисы, типы, стили, конфиги и утилиты. Набор сегментов не фиксирован — модуль включает только то, что реально нужно.

Модуль не обязан быть UI-блоком. Это может быть страница, виджет, бизнес-домен, инфраструктурный сервис или UI-kit сущность.

Главная граница модуля — не папка, а ответственность.

Компонент

Компонент — презентационная единица модуля, которая находится только в ui/ своего родительского модуля и отвечает за отображение части интерфейса.

Компонент не является архитектурной единицей: он не владеет сценарием, зависимостями, данными или внутренней структурой. Он работает только внутри границы родительского модуля.

Компонент отображает. Модуль организует.

Компонент не может:

  • Импортировать код проекта за пределами родительского модуля.
  • Владеть архитектурными зависимостями.
  • Содержать любые компоненты.
  • Содержать любые модули.
  • Делать внешние запросы.
  • Самостоятельно получать данные.
  • Выбирать источник данных.
  • Композировать данные.
  • Вызывать сценарные хуки.
  • Оркестрировать сценарий.
  • Композировать модули.
  • Решать, как устроен процесс.
  • Содержать бизнес-логику.
  • Содержать сценарную логику.

Если компоненту требуется что-то из этого списка, он перестаёт быть компонентом и должен быть оформлен как модуль.

auth/
├── ui/
│   └── logout-button/
│       ├── logout-button.tsx
│       ├── styles/
│       │   └── logout-button.module.css
│       ├── types/
│       │   └── logout-button-props.type.ts
│       └── index.ts
└── index.ts

Что считается модулем

Модулем считается папка, которая представляет самостоятельную область ответственности и имеет публичную границу.

Примеры модулей:

  • screens/home/ — модуль страницы.
  • widgets/page-heading/ — модуль виджета.
  • business/auth/ — модуль бизнес-домена.
  • infra/theme/ — модуль инфраструктурного сервиса.
  • ui/button/ — модуль UI-kit сущности.
  • screens/home/parts/hero-section/ — вложенный модуль страницы.

Не считаются модулями:

  • ui/, parts/, hooks/, types/, styles/, config/ — это сегменты.
  • screens/shop/, business/commerce/ — это группы, если в них нет index.ts.
  • screens/home/ui/user-card/ — это компонент, если он находится в ui/ и соблюдает ограничения компонента.

Типы модулей

Тип модуля определяет обязательный корневой файл и стартовую структуру.

UI-модуль

Модуль строится вокруг основного UI-компонента и обязан иметь основной .tsx файл в корне:

header/
├── header.tsx
└── index.ts

ui/ внутри такого модуля используется только для компонентов, которые помогают корневому .tsx файлу.

Бизнес-модуль

Бизнес-модуль — модуль, который строится вокруг публичного runtime API.

Бизнес-модуль обязан иметь фабрику в корне:

auth/
├── auth.factory.ts
├── index.ts
└── types/

Фабрика возвращает публичный runtime API модуля.

Инфраструктурный модуль

Инфраструктурный модуль — модуль, который строится вокруг технического сервиса или интеграции.

Инфраструктурный модуль не обязан иметь фиксированный корневой файл. Его структура определяется природой сервиса.

theme/
├── index.ts
├── config/
├── hooks/
├── styles/
└── ui/
backend-api/
├── backend-api.client.ts
├── config/
├── types/
└── index.ts

Структура

Модуль состоит из сегментов. Ни один сегмент не обязателен — модуль включает только те части, которые нужны его ответственности.

{module-name}/
├── {module-name}.factory.ts     # фабрика (для business-модулей)
├── {module-name}.tsx            # корневой файл модуля (опционален)
├── ui/                          # компоненты модуля
├── parts/                       # вложенные модули
├── hooks/                       # хуки
├── stores/                      # сторы состояния
├── services/                    # внешние источники данных
├── mappers/                     # трансформация данных между форматами
├── types/                       # типы
├── styles/                      # стили
├── lib/                         # утилиты модуля
├── config/                      # константы и конфигурация
└── index.ts                     # публичный API

Подробное описание сегментов — в разделе Сегменты.

Публичный API

Внешний код импортирует модуль только через публичный API.

// Хорошо
import { customerFactory } from '@/business/customer'
import type { Customer } from '@/business/customer'
// Плохо
import { validateToken } from '@/business/auth/lib/tokens'

index.ts модуля не обязан экспортировать всё содержимое. Он экспортирует только то, что действительно нужно снаружи.

Внутренние сегменты модуля остаются деталями реализации.

Business-модуль экспортирует из index.ts только фабрику и type-only экспорты. Хуки, компоненты, сервисы, мапперы и утилиты напрямую из index.ts не экспортируются — они доступны через API, который возвращает фабрика.

// business/customer/index.ts
export { customerFactory } from './customer.factory'

export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerDeps } from './types/customer-deps.type'
export type { CustomerFactory } from './types/customer-factory.type'

Фабрика

Business-модуль всегда экспортирует фабрику. Фабрика лежит в корне модуля ({name}.factory.ts), типизируется через {Name}Factory и возвращает публичный runtime API модуля.

Всё, что нужно внешнему коду в runtime, должно быть частью API, который возвращает фабрика.

Модуль без cross-domain зависимостей экспортирует фабрику без аргументов. Модуль с зависимостями — фабрику, принимающую зависимости доменными именами. Типы всегда экспортируются напрямую через export typeimport type не является runtime-зависимостью.

Компоновка фабрик происходит на уровне модуля-потребителя: screen, layout, widget или любой другой модуль группы «Композиция».

Структура business-модуля

business/customer/
├── customer.factory.ts
├── index.ts
└── types/
    ├── customer.type.ts
    ├── customer-api.type.ts
    ├── customer-deps.type.ts
    └── customer-factory.type.ts

Типы

// business/customer/types/customer-api.type.ts
export type CustomerApi = {
  useCustomer: () => Customer
  CustomerCard: (props: CustomerCardProps) => ReactNode
}
// business/order/types/order-deps.type.ts
export type OrderDeps = {
  customer: Pick<CustomerApi, 'useCustomer'>
}
// business/order/types/order-factory.type.ts
export type OrderFactory = (deps: OrderDeps) => OrderApi

Фабрика без зависимостей

// business/customer/customer.factory.ts
import type { CustomerFactory } from './types/customer-factory.type'

export const customerFactory: CustomerFactory = () => {
  return {
    useCustomer,
    CustomerCard,
  }
}

Фабрика с зависимостями

// business/order/order.factory.ts
import type { OrderFactory } from './types/order-factory.type'

export const orderFactory: OrderFactory = (deps) => {
  return {
    useOrder,
    OrderCard,
  }
}

Композиция на уровне screen

// screens/home/home.screen.tsx
import { customerFactory } from '@/business/customer'
import { orderFactory } from '@/business/order'

const customer = customerFactory()
const order = orderFactory({ customer })

const { useOrder, OrderCard } = order

export const HomeScreen = () => {
  const currentOrder = useOrder()

  return <OrderCard order={currentOrder} />
}

Жизненный цикл

Модуль рождается на самом низком уровне использования и поднимается выше только при реальной потребности.

  • Нужен на одной странице → screens/{name}/parts/
  • Появился в 2+ местах → поднимается по природе:
    • абстрактный UI → ui/
    • блок с данными/логикой → widgets/
    • представление бизнес-домена → business/{area}/parts/

Подъём — обычный рефакторинг в рамках задачи, а не отдельная активность.