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

Публичное API модуля приложения

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

Цели

Удобство использования и интеграции модуля достигается через выполнение ряда целей:

  1. Приложение должно быть защищено от изменений внутренней структуры отдельных модулей
  2. Переработка внутренней структуры модуля не должна затрагивать другие модули
  3. Существенные изменения поведения модуля должны быть легко определяемы

    Существенные изменения поведения модуля - изменения, ломающие ожидания сущностей-пользователей модуля.

Достичь этих целей позволяет введение публичного интерфейса (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 использовать более агрессивные оптимизации при работе с таким файлом

См. также