はじめに
こんにちは。FanTop事業本部 企画グループの安村です。この記事では、DI(Dependency Injection)フレームワークのuber-go/fxをFanTopに導入したことについてご紹介します。
導入の背景
NFTマーケットプレイスであるFanTopのバックエンドは、下図のようにブロックチェーンや決済サービス、コンテンツ配信といった多くの外部サービスと連携している特徴があります。
しかし、そのような特徴から依存性の管理が煩雑になることやコードが冗長になること、可読性が低い等の課題に直面することになりました。
こうした中で、DIフレームワークであるuber-go/fxがそれらの課題を解決できる可能性があると考え、今回導入することになりました。
以前のFanTopの状況
下図の左側は、以前のFanTopバックエンドの依存関係を図で表したものになります。これらを実装すると、図の右側のように「依存性注入」「エラーハンドリング」「サーバーの起動・停止」などの処理が必要になります。以前はこのコード量が100行以上あり、とても複雑な実装になっていました。
uber-go/fxについて
uber-go/fxは、Uberが開発したDIフレームワークで、UberのほぼすべてのGoサービスで使用されているため十分な実績があります。また、依存性の解決を実行時に逐次行うといった特徴や、一般的なDIに加えてシステムの「起動・停止」などのライフサイクルの管理と実行が可能であるといった特徴もあります。
他の代表的なDIツールとして「google/wire」も存在しますが、こちらは「依存性のコード」とそこから生成される「実行のコード」の管理が別になっています。これに対してuber-go/fxでは、「依存性のコード」と「実行のコード」が一致するため、管理するコードが最小限に抑えられるといった利点があります。
今回はuber-go/fxの機能の中から特に重要なものを3つピックアップしてご紹介します。
重要な機能その①「fx.Provide」
1つ目の機能は「fx.Provide」です。
こちらは、必要なファクトリ関数を列挙することで、それらの依存性の解決を自動で行う関数になります。下図は依存関係を解決する例を示しており、左側は依存性を手動で解決する場合の例です。この例では、DBのファクトリ関数でインスタンスを生成し、エラーチェックを行った後に次の依存関係先に渡すといった処理になっています。
fx.Provideを導入することによって、各ファクトリ関数をfx.Provideの引数に与えることで、ファクトリ関数の引数や戻り値の型を元にして自動的に依存関係を解決することが可能になります。これによって、エラーハンドリングの記述が不要になり、インスタンスの不足や重複といったバリデーションも自動的に行うことが可能になります。
重要な機能その②「fx.Lifecycle」
2つ目の機能は「fx.Lifecycle」です。
こちらはシステムのライフサイクル(起動と停止)の管理と実行を行う機能です。下図がサーバーの起動と停止のコード例を示したものです。上の黄色枠のOnStartでは、サーバー起動の関数を定義しており、下の黄色枠のOnStopでは、サーバー停止の関数を定義しています。
これらの、OnStart・OnStopをfx.Hookに定義し、fx.Lifecycleに追加することで、DIとあわせてライフサイクルも管理することが可能になります。
重要な機能その③「fx.Module」
最後は「fx.Module」についてです。
こちらは上述した「fx.Provide」や「fx.Lifecycle」といったものを各サービスごとにまとめて定義できる機能になります。 例えば下図のように、AWSのモジュールであれば、AWSサービスの初期化に関するファクトリ関数だけをまとめてfx.Provideに定義することができます。また、サーバのモジュールであれば、サーバの初期化に関するfx.Provideと起動停止を管理するfx.Lifecycleをまとめて定義できます。
fx.Moduleを用いることで、各サービスの境界線が明確になり、コードの可読性・保守性が向上する利点があります。また、システムに合わせて各モジュールをブロックのように組み合わせることができるため、再利用性が向上するといった利点もあります。
uber-go/fx導入後のFanTopの話
uber-go/fxをFanTopに導入することによって、以前まで100行以上あった依存解決のコードを10行以下までに短縮することができました。また、各モジュール内の記述においても、エラーハンドリングの処理が不要になることから、コード量を大幅に削減できました。
また、サービスの初期化に必要なファクトリ関数をモジュールで管理することで、サービスの境界線が明確になり、可読性が向上するといった結果も得られました。
さらに、コード量の削減や可読性の向上に加えて、開発スピードの向上といった利点もあります。既存サービスに新しく依存関係を追加する場合は、モジュール内のfx.Proviceにファクトリ関数を追加するだけでよく、新規サービスを追加する場合も新たにモジュールを作成して追加するだけでよくなります。これにより、依存関係の解決に必要な開発作業を最小限に抑えることができます。
終わりに
uber-go/fxの導入にあたり、個人的には学習コストや導入コストに対して得られる恩恵(コード量の削減や可読性・開発スピードの向上など)が非常に大きいと感じました。
今回の記事が、今後DIの導入を検討されている方の参考に少しでもなれば嬉しいです。