依存性注入
AdonisJSアプリケーションの中心には、ほぼゼロの設定でクラスを構築し、依存関係を解決できるIoCコンテナがあります。
IoCコンテナは次の2つの主要なユースケースを提供します。
- コンテナからバインディングを登録および解決するための、第一および第三者パッケージ向けのAPIの公開(後述のバインディングを参照)。
- クラスのコンストラクタまたはクラスメソッドに対して自動的に依存関係を解決し、注入する。
まずはクラスに依存関係を注入する方法から始めましょう。
基本的な例
自動的な依存性注入は、TypeScriptのレガシーデコレータの実装とリフレクションメタデータAPIに依存しています。
次の例では、EchoService
クラスを作成し、それをHomeController
クラスにインスタンスとして注入します。コード例をコピーして一緒に進めることができます。
ステップ1. サービスクラスを作成する
app/services
フォルダ内にEchoService
クラスを作成します。
export default class EchoService {
respond() {
return 'hello'
}
}
ステップ2. コントローラ内でサービスを注入する
app/controllers
フォルダ内に新しいHTTPコントローラを作成します。または、node ace make:controller home
コマンドを使用することもできます。
コントローラファイルでEchoService
をインポートし、コンストラクタの依存関係として受け入れます。
import EchoService from '#services/echo_service'
export default class HomeController {
constructor(protected echo: EchoService) {
}
handle() {
return this.echo.respond()
}
}
ステップ3. injectデコレータの使用
自動的な依存関係の解決を行うために、HomeController
クラスに@inject
デコレータを使用する必要があります。
import EchoService from '#services/echo_service'
import { inject } from '@adonisjs/core'
@inject()
export default class HomeController {
constructor(protected echo: EchoService) {
}
handle() {
return this.echo.respond()
}
}
以上です!HomeController
クラスをルートにバインドすると、自動的にEchoService
クラスのインスタンスが受け取られます。
結論
@inject
デコレータは、クラスのコンストラクタやメソッドの依存関係を観察し、コンテナにその情報を伝えるスパイのようなものと考えることができます。
AdonisJSルータがHomeController
の構築をコンテナに依頼するとき、コンテナは既にコントローラの依存関係を知っています。
依存関係のツリーの構築
現時点では、EchoService
クラスには依存関係がありませんし、コンテナを使用してそのインスタンスを作成することは過剰に思えるかもしれません。
クラスのコンストラクタを更新し、HttpContext
クラスのインスタンスを受け入れるようにしましょう。
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
@inject()
export default class EchoService {
constructor(protected ctx: HttpContext) {
}
respond() {
return `Hello from ${this.ctx.request.url()}`
}
}
再び、私たちはスパイ(@inject
デコレータ)をEchoService
クラスに配置して、その依存関係を検査する必要があります。
できます。それだけです。コントローラ内のコードを1行も変更せずに、コードを再実行すると、EchoService
クラスにHttpContext
クラスのインスタンスが渡されます。
コンテナを使用する利点の1つは、深くネストされた依存関係を持つことができ、コンテナがそのツリー全体を解決できることです。ただし、@inject
デコレータを使用する必要があります。
メソッドインジェクションの使用
メソッドインジェクションは、クラスメソッド内に依存関係を注入するために使用されます。メソッドインジェクションを使用するには、メソッドシグネチャの前に@inject
デコレータを配置する必要があります。
前の例を続けて、EchoService
の依存関係をHomeController
のコンストラクタからhandle
メソッドに移動しましょう。
コントローラ内でメソッドインジェクションを使用する場合、最初のパラメータは固定値(つまり、HTTPコンテキスト)を受け取り、残りのパラメータはコンテナを使用して解決されます。
import EchoService from '#services/echo_service'
import { inject } from '@adonisjs/core'
@inject()
export default class HomeController {
constructor(private echo: EchoService) {
}
@inject()
handle(ctx, echo: EchoService) {
return echo.respond()
}
}
以上です!今度は、EchoService
クラスのインスタンスがhandle
メソッド内に注入されます。
いつ依存性注入を使用するか
プロジェクトで依存性注入を活用することをオススメします。DIにより、アプリケーションのさまざまな部分間の緩い結合が作成されます。その結果、コードベースはテストやリファクタリングが容易になります。
ただし、依存性注入のアイデアを極端に取りすぎて、その利点を失わないように注意する必要があります。たとえば:
lodash
のようなヘルパーライブラリをクラスの依存関係として注入するべきではありません。直接インポートして使用してください。- コンポーネントが交換または置換される可能性のないコンポーネントには、緩い結合が必要ない場合があります。たとえば、
logger
サービスをインポートするか、Logger
クラスを依存関係として注入するかを選択できます。
コンテナを直接使用する
AdonisJSアプリケーション内のほとんどのクラス(Controllers、Middleware、Event listeners、Validators、Mailersなど)は、コンテナを使用して構築されます。そのため、@inject
デコレータを使用して自動的な依存性注入を活用できます。
コンテナを使用してクラスのインスタンスを自己構築する場合は、container.make
メソッドを使用できます。
container.make
メソッドは、クラスのコンストラクタを受け取り、その依存関係を解決した後にインスタンスを返します。
import { inject } from '@adonisjs/core'
import app from '@adonisjs/core/services/app'
class EchoService {}
@inject()
class SomeService {
constructor(public echo: EchoService) {}
}
/**
* 新しいクラスのインスタンスを作成するのと同じですが、
* 自動的なDIの利点があります
*/
const service = await app.container.make(SomeService)
console.log(service instanceof SomeService)
console.log(service.echo instanceof EchoService)
メソッド内で依存関係を注入するためにcontainer.call
メソッドを使用できます。container.call
メソッドは、次の引数を受け入れます。
- クラスのインスタンス。
- クラスインスタンスで実行するメソッドの名前。コンテナは依存関係を解決し、メソッドに渡します。
- メソッドに渡す固定パラメータのオプションの配列。
class EchoService {}
class SomeService {
@inject()
run(echo: EchoService) {
}
}
const service = await app.container.make(SomeService)
/**
* Echoクラスのインスタンスが
* runメソッドに渡されます
*/
await app.container.call(service, 'run')
コンテナのバインディング
コンテナのバインディングは、AdonisJSのIoCコンテナの存在理由の1つです。バインディングは、インストールしたパッケージとアプリケーション間の橋渡しとして機能します。
バインディングは、キーと値のペアであり、キーはバインディングの一意の識別子であり、値は値を返すファクトリ関数です。
- バインディング名は
string
、symbol
、またはクラスのコンストラクタであることができます。 - ファクトリ関数は非同期であることができ、値を返さなければなりません。
コンテナバインディングを登録するには、container.bind
メソッドを使用できます。以下は、コンテナからバインディングを登録および解決する簡単な例です。
import app from '@adonisjs/core/services/app'
class MyFakeCache {
get(key: string) {
return `${key}!`
}
}
app.container.bind('cache', function () {
return new MyCache()
})
const cache = await app.container.make('cache')
console.log(cache.get('foo')) // foo! を返します
コンテナバインディングを使用するタイミング
コンテナバインディングは、パッケージがエクスポートするシングルトンサービスを登録したり、自動的な依存性注入だけでは不十分な場合に使用されます。
すべてをコンテナに登録してアプリケーションを不必要に複雑にすることはオススメしません。代わりに、コンテナバインディングに手を出す前に、アプリケーションコード内の特定のユースケースを探してください。
以下は、フレームワークパッケージ内でコンテナバインディングを使用しているいくつかの例です。
- コンテナ内でBodyParserMiddlewareを登録する:ミドルウェアクラスは、
config/bodyparser.ts
ファイルに格納された構成が必要なため、自動的な依存性注入では機能しません。この場合、ミドルウェアクラスインスタンスを手動で構築するために、バインディングとして登録します。 - Encryptionサービスをシングルトンとして登録する:Encryptionクラスは、
config/app.ts
ファイルに格納されたappKey
が必要です。そのため、ユーザーアプリケーションからappKey
を読み取り、Encryptionクラスのシングルトンインスタンスを設定するためのブリッジとしてコンテナバインディングを使用します。
コンテナバインディングのコンセプトは、JavaScriptエコシステムでは一般的に使用されません。そのため、疑問点を明確にするために、Discordコミュニティに参加してください。
ファクトリ関数内でバインディングを解決する
バインディングファクトリ関数内で、コンテナから他のバインディングを解決できます。たとえば、MyFakeCache
クラスがconfig/cache.ts
ファイルから設定を必要とする場合、次のようにアクセスできます。
this.app.container.bind('cache', async (resolver) => {
const configService = await resolver.make('config')
const cacheConfig = configService.get<any>('cache')
return new MyFakeCache(cacheConfig)
})
シングルトン
シングルトンは、ファクトリ関数が1回呼び出され、その戻り値がアプリケーションのライフサイクルでキャッシュされるバインディングです。
container.singleton
メソッドを使用してシングルトンバインディングを登録できます。
this.app.container.singleton('cache', async (resolver) => {
const configService = await resolver.make('config')
const cacheConfig = configService.get<any>('cache')
return new MyFakeCache(cacheConfig)
})
値のバインディング
container.bindValue
メソッドを使用して、値を直接コンテナにバインドできます。
this.app.container.bindValue('cache', new MyFakeCache())
エイリアス
container.alias
メソッドを使用して、バインディングにエイリアスを定義できます。メソッドは、エイリアス名を最初のパラメータとして受け入れ、既存のバインディングまたはクラスのコンストラクタをエイリアス値として受け入れます。
this.app.container.singleton(MyFakeCache, async () => {
return new MyFakeCache()
})
this.app.container.alias('cache', MyFakeCache)
バインディングの静的な型を定義する
TypeScriptの宣言マージを使用して、バインディングの静的な型情報を定義できます。
型はContainerBindings
インターフェイス上でキーと値のペアとして定義されます。
declare module '@adonisjs/core/types' {
interface ContainerBindings {
cache: MyFakeCache
}
}
パッケージを作成する場合は、上記のコードブロックをサービスプロバイダファイル内に記述できます。
AdonisJSアプリケーションでは、上記のコードブロックをtypes/container.ts
ファイル内に記述できます。
抽象化レイヤーの作成
コンテナを使用すると、アプリケーションのための抽象化レイヤーを作成できます。インターフェイスをバインディングとして定義し、具体的な実装に解決できます。
この方法は、Hexagonal Architecture(ポートとアダプタの原則)をアプリケーションに適用したい場合に便利です。
TypeScriptのインターフェイスは実行時に存在しないため、インターフェイスの代わりに抽象クラスのコンストラクタを使用する必要があります。
export abstract class PaymentService {
abstract charge(amount: number): Promise<void>
abstract refund(amount: number): Promise<void>
}
次に、PaymentService
インターフェイスの具体的な実装を作成できます。
import { PaymentService } from '#contracts/payment_service'
export class StripePaymentService implements PaymentService {
async charge(amount: number) {
// Stripeを使用して金額を請求する
}
async refund(amount: number) {
// Stripeを使用して金額を返金する
}
}
これで、PaymentService
インターフェイスとStripePaymentService
具体的な実装をコンテナ内に登録できます。AppProvider
内で行います。
import { PaymentService } from '#contracts/payment_service'
export default class AppProvider {
async boot() {
const { StripePaymentService } = await import('#services/stripe_payment_service')
this.app.container.bind(PaymentService, () => {
return this.app.container.make(StripePaymentService)
})
}
}
最後に、コンテナからPaymentService
インターフェイスを解決し、アプリケーション内で使用できます。
import { PaymentService } from '#contracts/payment_service'
@inject()
export default class PaymentController {
constructor(private paymentService: PaymentService) {
}
async charge() {
await this.paymentService.charge(100)
// ...
}
}
テスト中の実装の切り替え
コンテナを使用して依存関係のツリーを解決する場合、そのツリー内のクラスに対してはほとんど/まったく制御を持っていません。そのため、それらのクラスをモック/フェイクすることはより困難になる場合があります。
次の例では、UsersController
のインスタンスメソッドindex
は、UserService
クラスのインスタンスを受け入れ、@inject
デコレータを使用して依存関係を解決しindex
メソッドに渡します。
import UserService from '#services/user_service'
import { inject } from '@adonisjs/core'
export default class UsersController {
@inject()
index(service: UserService) {}
}
テスト中に、実際のUserService
を使用したくない場合があります。なぜなら、それは外部のHTTPリクエストを行うためです。代わりに、フェイクな実装を使用したいと思います。
しかし、まずはUsersController
をテストするために書く可能性のあるコードを見てみましょう。
import UserService from '#services/user_service'
test('すべてのユーザーを取得する', async ({ client }) => {
const response = await client.get('/users')
response.assertBody({
data: [{ id: 1, username: 'virk' }]
})
})
上記のテストでは、HTTPリクエストを介してUsersController
とやり取りし、直接制御することはありません。
コンテナは、クラスをフェイクな実装と交換するための簡単なAPIを提供します。container.swap
メソッドを使用して交換を定義できます。
container.swap
メソッドは、交換したいクラスのコンストラクタを受け入れ、代替実装を返すファクトリ関数を続けて指定します。
import UserService from '#services/user_service'
import app from '@adonisjs/core/services/app'
test('すべてのユーザーを取得する', async ({ client }) => {
class FakeService extends UserService {
all() {
return [{ id: 1, username: 'virk' }]
}
}
app.container.swap(UserService, () => {
return new FakeService()
})
const response = await client.get('users')
response.assertBody({
data: [{ id: 1, username: 'virk' }]
})
})
交換が定義されると、コンテナは実際のクラスの代わりにそれを使用します。元の実装に戻すには、container.restore
メソッドを使用します。
app.container.restore(UserService)
// UserServiceとPostServiceのみを元に戻す
app.container.restoreAll([UserService, PostService])
// 全てを元に戻す
app.container.restoreAll()
コンテキスト依存関係
コンテキスト依存関係を使用すると、特定のクラスの依存関係をどのように解決するかを定義できます。たとえば、2つのサービスがDrive Disk
クラスに依存している場合を考えてみましょう。
import { Disk } from '@adonisjs/drive'
export default class UserService {
constructor(protected disk: Disk) {}
}
import { Disk } from '@adonisjs/drive'
export default class PostService {
constructor(protected disk: Disk) {}
}
UserService
にはGCSドライバを使用するディスクインスタンスを渡し、PostService
にはS3ドライバを使用するディスクインスタンスを渡したいとします。これは、コンテキスト依存関係を使用して行うことができます。
次のコードは、サービスプロバイダのregister
メソッド内に書かれる必要があります。
import { Disk } from '@adonisjs/drive'
import UserService from '#services/user_service'
import PostService from '#services/post_service'
import { ApplicationService } from '@adonisjs/core/types'
export default class AppProvider {
constructor(protected app: ApplicationService) {}
register() {
this.app.container
.when(UserService)
.asksFor(Disk)
.provide(async (resolver) => {
const driveManager = await resolver.make('drive')
return drive.use('gcs')
})
this.app.container
.when(PostService)
.asksFor(Disk)
.provide(async (resolver) => {
const driveManager = await resolver.make('drive')
return drive.use('s3')
})
}
}
コンテナフック
コンテナのresolving
フックを使用して、container.make
メソッドの戻り値を変更/拡張できます。
通常、特定のバインディングを拡張しようとするときに、サービスプロバイダ内でフックを使用します。たとえば、データベースプロバイダは、追加のデータベース駆動型のバリデーションルールを登録するためにresolving
フックを使用します。
import { ApplicationService } from '@adonisjs/core/types'
export default class DatabaseProvider {
constructor(protected app: ApplicationService) {
}
async boot() {
this.app.container.resolving('validator', (validator) => {
validator.rule('unique', implementation)
validator.rule('exists', implementation)
})
}
}
コンテナイベント
コンテナは、バインディングの解決またはクラスインスタンスの構築後にcontainer_binding:resolved
イベントを発行します。event.binding
プロパティは文字列(バインディング名)またはクラスコンストラクタであり、event.value
プロパティは解決された値です。
import emitter from '@adonisjs/core/services/emitter'
emitter.on('container_binding:resolved', (event) => {
console.log(event.binding)
console.log(event.value)
})
関連情報
- The container README file は、フレームワークに依存しない方法でコンテナのAPIをカバーしています。
- Why do you need an IoC container? この記事では、フレームワークの作成者がIoCコンテナを使用する理由について説明しています。