エンジニアリンググループの冨岡です。
TypeScript向けのDIライブラリを公開したので紹介します。
モチベーション
Clean Architecture等の設計手法を使ってコードを書いていると、抽象に依存することが多くなってきます。
class CreateUserAccount { constructor(private userAccountRepository: UserAccountRepository) {} } const forProduction = new CreateUserAccount( new PostgreSQLUserAccountRepository( buildPostgreSQLConfig(process.env) ) ) const forTest = new CreateUserAccount(new InMemoryUserAccountRepository());
テストしやすく、また責務がはっきりわかれていて良いですよね。ただ、別の辛みもいくつかでてきます。
- インスタンスの組み立てが巨大で、かつ順番に依存にした複雑な処理になる
- 本番用とテスト用など、共有したい部分とそうでない部分がでてきてうまく管理するのが面倒になってくる
こういった辛みに対応するのがDIライブラリです。しかし私の調べた限りでは、既存のDIライブラリは少し大げさな印象でした。 例えばメジャーなライブラリであるInversifyJSは、decoratorをベースにしており大げさ&マジカルです。
依存先を容易に切り替えられる既存のライブラリの利点は残しつつ、以下を満たすシンプルなライブラリがほしいと思いました。
- 依存をシンプルに表現できる
- 変な魔法を使わず、明示的である
そこで作ったのがこのtypesafe-diです。
機能と特徴の紹介
まずはDesignという設計図のようなものを作ります。基本的には、あるキーと、紐づくインスタンス生成に必要なfactoryのペアを持ったimmutableなオブジェクトです。
type HasName = { name: string }; type HasAge = { age: number }; const design = Design.empty .bind('age', () => 30) // Injector<{ [key]: T }> は { [key]: Promise<T> } の単なる型エイリアスです。 // ここでは、'user'を作るのに { name: string, age: number } が必要なことを型で明示しています。 .bind('user', async (injector: Injector<HasName & HasAge>) => new User( await injector.name, await injector.age, ));
最後に未解決の依存を注入することで、コンテナを生成します。
const { container } = await design.resolve({ name: 'jooohn' }); // { name: 'jooohn', age: 30, user: User { name: 'jooohn', age: 30 } }
この際、必要な依存が足りていないとコンパイルエラーになるのがポイントです。
// [compile error!] Property 'name' is missing in type {} const { container } = await design.resolve({});
これがコンパイルエラーになってくれることで、未完成なDesignを安全に扱うことができます。
// adapterに依存している。このままではインスタンス化できないが、再利用できる。 const useCasesDesign = Design.empty .bind('useCases', (injector: Injector<HasAdapters>) => ???) const productionDesign = useCasesDesign.merge(productionAdapterDesign); const testDesign = useCasesDesign.merge(testAdapterDesign);
適度にコードを分割して、型安全にDesignを整理することができそうです!
また、リソースの開放のための仕組みも用意しています。依存関係に従って、依存元から順番に終了処理を行うことでお行儀よくリソースの後始末ができます。
const design = Design.empty // 第3引数がoptionalな終了処理 (t: T) => Promise<void> .bind('db', () => buildDB(), db => db.close) .bind( 'repository', async (injector: Injector<HasDB>) => new Repository(await injector.db), repository => repository.close() ); const { container, finalize } = design.resolve({}); // repositoryの終了処理 => dbの終了処理 の順番で呼ばれる。 await finalize();
まとめ
TypeScriptを使ったシンプルなDIライブラリを作りました。結構使えると思うので、是非使ってみてください!もちろん、コントリビュートも大歓迎です!
エンジニア募集です!
エムスリーでは一緒にDepdencyをInjectしてくれるエンジニアを募集しています!