Ray.Di メンタルモデル

KeyProvider、それに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);
   }
}
  • MessageProviderget()メソッドが呼び出され ‘hello world’を返します。
  • CountProviderget() メソッドが呼び出され3を返します。

Ray.Diの使用

Ray.Diの利用には2つのパートがあります。

  1. コンフィギュレーション:アプリケーションが”Ray.Diマップ”に何か追加します。
  2. インジェクション:アプリケーションが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)と表されるのはこのためです。

何かを必要とすることを宣言するにはいくつか方法があります。

  1. コンストラクターの引数:

     class Foo
     {
       // どこからかデータベースが必要
       public function __construct(
             private Database $database
        ) {}
     }
    
  2. 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 マップにエントリを追加する方法について学びましょう。


  1. PHPの配列はマップです。また、Ray.Diの実際の実装ははるかに複雑ですが、マップはRay.Diがどのように動作するかおおよそを表しています。 

  2. 頂点と向きを持つ辺(矢印)により構成されたグラフです。 

  3. その逆もまた然りで、何も使わなくても、何かを提供することは問題ありません。とはいえ、デッドコードと同じように、どこからも使われなくなったプロバイダーは削除するのが一番です。