公開API
FSDでの各エンティティは、使いやすく統合しやすいモジュールとして設計されています。
目的
モジュールの使いやすさと統合しやすさは、いくつかの目的を達成することによって実現されます。
- アプリケーションは、個々モジュール内部構造の変更から保護されるべき
- モジュール内部構造の再設計は、他のモジュールに影響を与えてはならない
- モジュールの動作の重要な変更は、簡単に特定できるべき
モジュールの動作に関する重要な変更とは、モジュール利用者の期待を壊す変更
これらの目的を達成するために、公開API(公開インターフェース)が導入され、モジュール機能への単一アクセス点を提供し、モジュールと外部世界との相互作用の「契約」を定義します。
エンティティの構造は、公開APIを提供する単一のエントリーポイントを持つべき
└── features/ #
└── auth-form/ # フィーチャーの内部構造
├── ui/ #
├── model/ #
├── {...}/ #
└── index.ts # フィーチャーの公開APIを持つエントリーポイント
export { Form as AuthForm } from "./ui"
export * as authFormModel from "./model"
公開APIの要件
下記の要件を満たすことで、モジュールとの相互作用を公開API契約の実行に制限し、モジュールの信頼性と使いやすさを達成できます。
1. アクセス制御
公開APIは、モジュール内容へのアクセス制御を行うべきです。
- アプリケーションの他の部分は、公開APIで提供されるモジュールのエンティティのみを使用できる
- 公開APIのないモジュールの内部部分は、モジュール自身のみがアクセスできる
例
プライベートインポートからの排除
-
悪い例: 公開APIをバイパスしてモジュールの内部部分に直接アクセスすることは危険であり、特にモジュールのリファクタリング時に問題を引き起こす可能性がある。
- import { Form } from "features/auth-form/components/view/form"
-
良い例: APIは事前に必要なものだけをエクスポートし、モジュールの開発者はリファクタリング時に公開APIを壊さないことだけを考えればよい。
+ import { AuthForm } from "features/auth-form"
2. 変更への耐性
公開APIは、モジュール内部の変更に対して耐性があるべきです。
- モジュールの動作を壊す変更は、公開APIの変更として反映されるべき
例
実装からの抽象化
内部構造の変更は、公開APIの変更を引き起こすべきではありません。
-
悪い例: このコンポーネントをフィーチャー内で移動、または名前変更すると、すべての使用場所でインポートをリファクタリングする必要が生じる。
- import { Form } from "features/auth-form/ui/form"
-
良い例: フィーチャーのインターフェースは内部構造を反映せず、外部の「ユーザー」はフィーチャー内のコンポーネントの移動や名前変更の影響を受けない。
+ import { AuthForm } from "features/auth-form"
3. 統合性
公開APIは、簡単で柔軟な統合を促進するべきです。
- 公開APIは、アプリケーションの他の部分での使用が便利であり、特に名前衝突問題を解決する必要がある。
例
名前の衝突
-
悪い例: 名前の衝突が発生してしまう。
features/auth-form/index.tsexport { Form } from "./ui"
export * as model from "./model"features/post-form/index.tsexport { Form } from "./ui"
export * as model from "./model"- import { Form, model } from "features/auth-form"
- import { Form, model } from "features/post-form" -
良い例: インターフェースレベルで名前の衝突が解決される。
features/auth-form/index.tsexport { Form as AuthForm } from "./ui"
export * as authFormModel from "./model"features/post-form/index.tsexport { 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
衝突の解決
名前の衝突は、実装レベルではなく公開APIのレベルで解決されるべきです。
-
悪い例: 名前の衝突が実装レベルで解決される。
features/auth-form/index.tsexport { AuthForm } from "./ui"
export { authFormActions, authFormReducer } from "model"features/post-form/index.tsexport { PostForm } from "./ui"
export { postFormActions, postFormReducer } from "model" -
良い例: 名前の衝突がインターフェースレベルで解決される。
features/auth-form/model.tsexport { actions, reducer }
features/auth-form/index.tsexport { Form as AuthForm } from "./ui"
export * as authFormModel from "./model"features/post-form/model.tsexport { actions, reducer }
features/post-form/index.tsexport { Form as PostForm } from "./ui"
export * as postFormModel from "./model"
再エクスポートについて
JavaScriptでは、モジュールの公開APIは、モジュール内部のエンティティをindex
ファイルで再エクスポートすることによって作成されます。
export { Form as AuthForm } from "./ui"
export * as authModel from "./model"
欠点
-
ほとんどの人気のバンドラーでは、再エクスポートのためにコード分割の効果が低下してしまいます。なぜなら、このアプローチではツリーシェイキングが安全にモジュール全体しか削除することができないからです。
例えば、ページモデルで
authModel
をインポートすると、たとえ使用されていなくても、AuthForm
コンポーネントがそのページのチャンクに含まれてしまいます。 -
結果として、チャンクの初期化が高コストになり、ブラウザはその中のすべてのモジュールを処理する必要があります。
可能な解決策
webpack
は、再エクスポートファイルを副作用なしとしてマークすることを可能にしています。これにより、webpack
はそのファイルを扱う際に攻撃的な最適化を使用できるようになります。