Публичное API модуля приложения
Каждая сущность методологии проектируется как удобный в использовании и интеграции модуль.
Цели
Удобство использования и интеграции модуля достигается через выполнение ряда целей:
- Приложение должно быть защищено от изменений внутренней структуры отдельных модулей
- Переработка внутренней структуры модуля не должна затрагивать другие модули
- Существенные изменения поведения модуля должны быть легко определяемы
Существенные изменения поведения модуля - изменения, ломающие ожидания сущностей-пользователей модуля.
Достичь этих целей позволяет введение публичного интерфейса (Public API), представляющего собой единую точку доступа к возможностям модуля и определяющего "контракт" взаимодействия модуля с внешним миром.
Структура сущности должна иметь единую точку входа, предоставляющую публичный интерфейс
└── features/ #
└── auth-form/ # Внутренняя структура фичи
├── ui/ #
├── model/ #
├── {...}/ #
└── index.ts # Энтрипоинт фичи с ее публичным API
export { Form as AuthForm } from "./ui"
export * as authFormModel from "./model"
Требования к публичному API
Выполнение этих требований позволяет свести взаимодействие с модулем к выполнению публичного интерфейса-контракта и, тем самым, достичь надежности и удобства в использовании модуля.
1. Контроль доступа
Public API должен осуществлять контроль доступа к содержимому модуля
- Другие части приложения могут использовать только те сущности модуля, которые представлены в публичном интерфейсе
- Внутренняя часть модуля за пределами публичного интерфейса доступны только самому модулю.
Примеры
Отстранение от приватных импортов
-
Плохо: Идет обращение напрямую к внутренним частям модуля, минуя публичный интерфейс доступа - опасно, особенно при рефакторинге модуля
- import { Form } from "features/auth-form/components/view/form"
-
Хорошо: API заранее экспортирует только нужное и разрешенное, разработчику модуля теперь нужно думать только о том, чтобы не ломать Public API при рефакторинге
+ import { AuthForm } from "features/auth-form"
2. Устойчивость к изменениям
Public API должен быть устойчивым к изменениям внутри модуля
- Изменения, ломающие поведения модуля, должны отражаться в изменении Public API
Примеры
Абстрагирование от реализации
Изменение внутренней структуры не должно приводить к изменению Public API
-
Плохо: перемещение или переименование этого компонента внутри фичи приведет к необходимости рефакторить импорты во всех местах использования компонента.
- import { Form } from "features/auth-form/ui/form"
-
Хорошо: интерфейс фичи не отображает её внутреннюю структуру, внешние "пользователи" фичи не пострадают от перемещения или переименования компонента внутри фичи
+ import { AuthForm } from "features/auth-form"
3. Интегрируемость
Public API должен способствовать легкой и гибкой интеграции
- Должен быть удобен для использования остальными частями приложения, в частности, решать проблему колли зии имен
Примеры
Коллизия имен
-
Плохо: будет коллизия имен
export { Form } from "./ui" export * as model from "./model"
export { Form } from "./ui" export * as model from "./model"
- import { Form, model } from "features/auth-form" - import { Form, model } from "features/post-form"
-
Хорошо: коллизия решена на уровне интерфейса
export { Form as AuthForm } from "./ui" export * as authFormModel from "./model"
export { Form as PostForm } from "./ui" export * as postFormModel from "./model"
+ import { AuthForm, authFormModel } from "features/auth-form" + import { PostForm, postFormModel } from "features/post-form"
Гибкое использование
-
Плохо: неудобно писать, неудобно читать, "пользователь" фичи страдает
- import { storeActionUpdateUserDetails } from "features/auth-form" - dispatch(storeActionUpdateUserDetails(...))
-
Хорошо: "пользователь" фичи получает доступ к нужным вещам итеративно и гибко
+ import { authFormModel } from "features/auth-form" + dispatch(authFormModel.effects.updateUserDetails(...)) // redux + authFormModel.updateUserDetailsFx(...) // effector
Разрешение коллизий
Коллизия имен должна решаться на уровне публичного интерфейса, а не реализации
-
Плохо: коллизия имен решается на уровне реализации
export { AuthForm } from "./ui" export { authFormActions, authFormReducer } from "model"
export { PostForm } from "./ui" export { postFormActions, postFormReducer } from "model"
-
Хорошо: коллизия имен решается на уровне интерфейса
export { actions, reducer }
export { Form as AuthForm } from "./ui" export * as authFormModel from "./model"
export { actions, reducer }
export { Form as PostForm } from "./ui" export * as postFormModel from "./model"
О реэкспортах
В JavaScript публичный интерфейс модуля создается с помощью реэкспорта сущностей изнутри модуля в index
файле:
export { Form as AuthForm } from "./ui"
export * as authModel from "./model"
Недостатки
-
В большинстве популярных бандлеров из-за реэкспортов хуже работает код-сплиттинг, т.к. tree-shaking при таком подходе может безопасно отбросить только модуль целиком, но не его часть.
Например, импорт
authModel
в модели страницы приведет к попаданию компонентаAuthForm
в чанк этой страницы, даже если этот компонент там не используется. -
Как следствие, инициализация чанка становится дороже, т.к. браузер должен обработать все модули в нем, в том числе и те, что попали в бандл "за компанию"
Возможные пути решения
webpack
позволяет отметить файлы-реэкспорты как side effects free - это разрешаетwebpack
использовать более агрессивные оптимизации при работе с таким файлом