フロントエンド におけるレイヤードアーキテクチャの導入
こんにちは、株式会社POLでエンジニアをしているミズノです。以前弊社の雑談イベントでフロントエンド の取り組みについて話したのですが、そこでお話しした内容を今回は紹介したいと思います。
元々POLのフロントエンド は、よく見るReactのアーキテクチャでした。
React + Redux
プロダクトも徐々に大きくなり、開発者も増えるなかで色々な問題が見えてきました。特に以下のような問題が多く見受けられました。
1.複数人で開発する場合ルールがないと迷いが発生する
2.コード自体の結合度が強く、テストがしづらい
3.修正するときに影響範囲の調査が大変
こんな感じで、余計なところで頭悩ませることが多いなと感じていました。そこで迷わず課題に取り組める、そしてプロダクがさらに大きくなった場合にも耐えれる構造を検討し、レイヤードアーキテクチャを試すことにしました。
導入にあたっては、新規のページから導入し、少しづつ周りのメンバーにも布教、巻き込む形を取っています。
ディレクトリ構成
- infrastructure
- domain
- application
- presentation
まずは骨格となるディレクトリを以上のように定義しました。それぞれのディレクトリが独立した階層になり、infrastructure層にはdomain層経由でアクセスします。レイヤ分けることで、各レイヤーの責務が明確になり依存を最小限に止めることができます。
簡単な例があるとわかりやすいと思うので紹介します。サンプルコードはこちらにあります。
実践例: infrastructure
class UserApiDriver implements UserDriver {
// 実装は省略
fetch(): Promise<any> // 型定義はサンプルなのでanyにしています。
}
例えば、APIからユーザー情報の取得では、infrastructure層にdriverを作成します。driverは外部との連携を担当します。ここでは外部APIからデータを取得する処理を書いたりします。
実践例: domain
class UserRepository implements ReadUserRepository {
constructor(driver: UserDriver){
this._driver = driver;
}
read(): Promise<any> {
const response = this._driver.fetch();
// 以下省略
}
}
repositoryではDriverを利用してデータを取得します。repositoryではドライバのメソッド実行し、レスポンスデータをドメインデータに変換する役目を持ってもらってます。でも割とここが一番迷いが発生しがちな場所です。
実践例:application
class GetUserDataUseCase implements GetUserData {
// リポジトリを注入
constructor(repository: ReadUser){
this._repository = repository
}
//
handle():Promise<any>{
//repository経由でデータを取得して、ごにょごにょする
};
}
applicationではロジックを持つことが多いですが、今回はデータを取得するだけなので、特にロジックらしいものはありませんが、repositoryのメソッドを実行しデータを取得します。applicationディレクトリ内にはXXXUseCaseという形で色々な処理があります。
ここまで下準備みたいなものです。手間かかるので、めんどくさいですよね。コード自動生成したりである程度緩和はできますが、依存オブジェクトを注入する形なので、テストも簡単にかけますし、何をするのかがわかりやすいので受けるメリットの方が大きいと現状は判断しています。
実践例:Reactでの利用
いよいよReactコンポーネントでこれまで準備したコードを利用します。Reactはpresentation層で利用します。presentationからはhooksを経由してapplication層のコードを利用します。 hooksで利用するときにはapplication層のコードを直接importするのではなく、context経由で取得します。これでhooksでも簡単にDIを実現することができます。importは簡単に依存を増やすので利用するときは注意が必要です。ただし同一レイヤーのimportは特に制限はしません。あくまでも別レイヤーのコードを直接インポートするのをNGにしています。
const useGetUser = () => {
const {GetUserDataUseCase} = useContext(dependencyContext);
useEffect(() => {
(async () => {
const result = await GetUserDataUseCase.handle();
// 省略
})()
}, []);
}
上位レイヤーのコードをpresentationで利用するところは、Pure DIという考え方を利用しています。これはVanila DIと呼ばれることもありますが、SwiftライブラリのPureDIの実装を参考にしています。
Pure DI
やり方はいたって簡単です。依存の解決場所を一箇所にまとめて、context経由で依存オブジェクトを渡しているだけです。依存の解決はCompositionRootという場所でまとめています。
class CompositionRoot {
public static resolve(): CompositionRoot {
return new CompositionRoot();
}
// ここで依存を解決
public get GetUserData: GetUserData {
return new GetUserDataUseCase(
new UserRepository(
new UserApiDriver()
)
)
}
}
export default conmosition = CompositionRoot.resolve();
APIの開発が終わってなければ以下のようにUserMockDriverなどを簡単に利用することができます。
class CompositionRoot {
public static resolve(): CompositionRoot {
return new CompositionRoot();
}
// ここで依存を解決
public get GetUserData: GetUserData {
return new GetUserDataUseCase(
new UserRepository(
new UserMockDriver() // mockドライバーを利用
)
)
}
}
export default conmosition = CompositionRoot.resolve();
レイヤーの効果
ここまでのコードでお気づきかもしれませんが、Reactコンポーネントとそれ以外の層が完全に分かれています。フロントエンド の開発では、UIの変更は頻繁に起こるが、ロジックの変更はそこまで多くはないと思います。そのため、presentation層を簡単に入れ替えることができるのは開発においてメリットがあり、テスト範囲も局所的に絞り込むこともできます。
実はpresentation層においてはもう一つ、Micro Atomic Designという考え方を導入しています。こちらは次回の記事で紹介します。
まとめ
今回の取り組みも、すべての課題を解決するわけではないですしベストかどうかもわかりませんが、試行錯誤しながら日々開発を楽しんでいます。そして社内では新しい取り組みは奨励していますし、一緒に試行錯誤してくれるメンバーを募集中ですので、この記事で興味持っていただければ是非カジュアル面談でミズノをご指名ください!