Ray.Di メンタルモデル
Key
やProvider
、それにRay.Diがどうのようにして単なるマップと考えられるかについて
依存性注入(Dependency Injection)について調べていると、多くのバズワード(”制御の反転”、”ハリウッド原則”、”インジェクション”)を目にし混乱することがあります。しかし、依存性注入という専門用語に対してコンセプトはそれほど複雑ではありません。実際、あなたはすでによく似たことを書いているかもしれません。 このページでは、Ray.Diの実装の簡略化されたモデルについて説明しどのように働くかの理解を助けます。
Ray.Diはマップ
基本的にRay.Diは、アプリケーションが使用するオブジェクトの作成と取得を支援します。アプリケーションが必要とするこれらのオブジェクトは 依存や依存性(dependencies) と呼ばれます。
Ray.Diはマップ1であると考えることができます。アプリケーションのコードが必要な依存関係を宣言すると、Ray.Diはそのマップからそれらを取得します。”Ray.Diマップ”の各エントリーは、2つの部分から構成されています。
- Ray.Di キー: マップから特定の値を取得するために使用されるマップのキー
- プロバイダー: アプリケーションのオブジェクトを作成するために使用されるマップの値
Ray.Diキー
Ray.DiはKey
を使って、Ray.Diマップから依存関係を解決します。
はじめに で使用されている Greeter
クラスは、コンストラクターで2つの依存関係を宣言していて、それらの依存関係は Ray.Di では Key
として表されます。
#[Message] string
–>$map[$messageKey]
#[Count] int
–>$map[$countKey]
最も単純な形の Key
は、PHP の型で表されます。
// 依存(文字列)を特定
/** @var string $databaseKey */
$databaseKey = $map[$key];
しかし、アプリケーションには同じ型の依存関係があることがあります。
class Message
{
public function __construct(
public readonly string $text
){}
}
class MultilingualGreeter
{
public function __construct(
private readonly Message $englishGreeting,
private readonly Message $spanishGreeting
) {}
}
Ray.Diでは、同じタイプの依存関係を区別するために、アトリビュート束縛 を使用しています。
class MultilingualGreeter
{
public function __construct(
#[English] private readonly Message $englishGreeting,
#[Spanish] private readonly Message $spanishGreeting
) {}
}
束縛アノテーションを持つ Key
は、次のように作成することができます。
$englishGreetingKey = $map[Message::class . English::class];
$spanishGreetingKey = $map[Message::class . Spanish::class];
アプリケーションが $injector->getInstance(MultilingualGreeter::class)
を呼び出したとき、
MultilingualGreeter
のインスタンスを生成しますが、以下と同じ事を行っています。
// Ray.Diは内部でこれを行うので、手動でこれらの依存関係を解決する必要はありません。
$english = $injector->getInstance(Message::class, English::class));
$spanish = $injector->getInstance(Message::class, Spanish::class));
$greeter = new MultilingualGreeter($english, $spanish);
つまりRay.Diの Key
はPHPの型と依存関係を識別するためのアトリビュート(オプション)を合わせたものです。
Ray.Diプロバイダー
Ray.Diでは依存関係を満たすオブジェクトを生成するファクトリーのために、”Ray.Diマップ”でProviderInterface
を使用します。
Provider
は単一のメソッドを持つインターフェースです。
interface ProviderInterface
{
/** インスタンスを用意する */
public function get();
}
ProviderInterface
を実装している各クラスは、 インスタンスを生成する方法を知っている簡単なコードです。new
を呼び出したり、他の方法で依存を構築したり、キャッシュから事前に計算されたインスタンスを返したりすることができます。値の型は限定されずmixedです。
以下は 2 つの ProviderInterface
の実装例です。
class countProvicer implements ProviderInterface
{
public function get(): int
{
return 3;
}
}
class messageProvider implements ProviderInterface
{
public function get(): string
{
return 'hello world';
}
}
class DemoModule extends AbstractModule
{
protected function configure(): void
{
$this->bind()->annotatedWith(Count::class)->toProvider(CountProvicer::class);
$this->bind()->annotatedWith(Message::class)->toProvider(MessageProvicer::class);
}
}
MessageProvider
のget()
メソッドが呼び出され ‘hello world’を返します。CountProvider
のget()
メソッドが呼び出され3を返します。
Ray.Diの使用
Ray.Diの利用には2つのパートがあります。
- コンフィギュレーション:アプリケーションが”Ray.Diマップ”に何か追加します。
- インジェクション:アプリケーションがRay.Diにマップからのオブジェクトの作成と取得を依頼します。
以下に説明します。
コンフィギュレーション
Ray.Diのマップは、Ray.Diモジュールを使って設定されます。Ray.Diモジュールは、Ray.Diマップに何かを追加する設定ロジックユニットです。Ray.Di ドメイン固有言語(DSL)を使用して設定を行います。
これらのAPIは単にRay.Dマップを操作する方法を提供するものです。これらのAPIが行う操作は簡単で、以下は簡潔なPHPのシンタックスを使用した説明です。
Ray.Di DSL シンタックス | メンタルモデル |
---|---|
bind($key)->toInstance($value) |
$map[$key] = $value; (インスタンス束縛) |
bind($key)->toProvider($provider) |
$map[$key] = fn => $value; (プロバイダー束縛) |
bind(key)->to(anotherKey) |
$map[$key] = $map[$anotherKey]; (リンク束縛) |
DemoModule
は Ray.Di マップに2つのエントリーを追加します。
#[Message] string
–>(new MessageProvider())->get()
#[Count] int
–>(new CountProvider())->get()
インジェクション
マップから物事を プル するのではなく、それらが必要であることを 宣言 します。これが依存性注入の本質です。何かが必要なとき、どこかからそれを取りに行ったり、クラスから何かを返してもらったりすることはありません。その代わりにあなたは単にそれなしでは仕事ができないと宣言し、必要とするものを与えるのがRay.Diの役割です。
このモデルは、多くの人がコードについて考える方法とは逆で、「命令的」ではなく「宣言的」なモデルと言えます。依存性注入がしばしば一種の制御の反転 (IoC)と表されるのはこのためです。
何かを必要とすることを宣言するにはいくつか方法があります。
-
コンストラクターの引数:
class Foo { // どこからかデータベースが必要 public function __construct( private Database $database ) {} }
-
Provider
コンストラクターの引数class DatabaseProvider implements ProviderInterface { public function __construct( #[Dsn] private string $dsn ){} public function get(): Database { return new Database($this->dsn); } }
この例は、はじめにのサンプルFooクラスと同じです。
注:Ray.Di は Guice とは異なり、コンストラクターに _Inject_は必要はありません。
依存関係がグラフを形成
それ自体に依存性があるものを注入する場合、Ray.Diは再帰的に依存関係を注入します。上記のような Foo
のインスタンスをインジェクトするために、Ray.Di は以下のような ProviderInterface
の実装を作成することが考えられます。
class FooProvider implements ProviderInterface
{
public function get(): Foo
{
global $map;
$databaseProvider = $map[Database::class]);
$database = $databaseProvider->get();
return new Foo($database);
}
}
class DatabaseProvider implements Provider
{
public function get(): Database
{
global $map;
$dsnProvider = $map[Dsn::class];
$dsn = $dsnProvider->get();
return new Database($dsn);
}
}
class DsnProvider implements Provider
{
public function get(): string
{
return getenv(DB_DSN);
}
}
依存関係は 有向グラフ2 を形成し、インジェクションは、必要なオブジェクトからそのすべての依存関係を介してグラフの深さ優先探索を実行することによって動作します。
Ray.Di の Injector
オブジェクトは、依存関係グラフ全体を表します。インジェクター
を作成するために、Ray.Diはグラフ全体が動作することを検証する必要があります。依存関係が必要なのに提供されていない「ぶら下がり」ノードがあってはいけません3 もしグラフのどこかで束縛が不完全だと、Ray.Di は Unbound
例外を投げます。
次に
Ray.Di が作成したオブジェクトのライフサイクルを管理するための Scopes の使い方と、さまざまなRay.Di マップにエントリを追加する方法について学びましょう。