この記事は BEAR.Sunday Advent Calendar 2021の記事です。
弁護士ドットコムでは筆者がJoinする前からRay.Diを2つのプロダクトで利用していました。(現在はもっと増えています)。社内用のWikiにアップした記事を、許可を得てここに転載します。READMEの内容と合わせて、背景技術や比較を加えています。使い方だけでなく、より技術的な理解が深まるものをと考え記載しました。
Ray.Diとは
-
Ray.DiはGoogle GuiceのPHP版として開発したPHPのDIフレームワークです。
-
DIではオブジェクトを作るのに必要なオブジェクトや値を依存としてコンストラクタで受け取ります。その依存を作るためにも、他の依存が必要です。そうやってオブジェクトを作るのには結局オブジェクトのグラフが必要になります。オブジェクトグラフを手動で作成するのは骨の折れる仕事です。テストも難しくなります。代わりにRay.Diがオブジェクトグラフを作ります。 (Guiceマニュアルより抜粋)
DIとは
DIとは疎結合のコーディングを可能にするソフトウエアデザイン原則とパターンのセットです。
-
プログラムで使用する定数をコード内でハードコードしないように、使用するオブジェクトも new {クラス名} などとハードコードして生成しないでインターフェイスを通じてコンストラクタなどのメソッド通じて受け取ります。定数はdefineやconfigファイルなどコードと別にしてプログラムの柔軟性を高めますが、それをオブジェクトに広げたものです。
-
コードが必要とするオブジェクトや値を依存 (dependency)と呼びます。newやfactoryメソッドなどで、生成、取得(pull)するのではなく、メソッドを通じて代入してもらう事(push=injection)を期待します。これを依存性の注入、Dependency Injection = DIと呼びます。 ソフトウエア原則、及びパターンの事です。
-
2004年にソフトウエアアーキテクトのマーティンファウラーがInversion of Control Containers and the Dependency Injection patternという記事の中でこのソフトウエアパターンをDIと名付けました。
-
DIの目的はコードの再利用性とテスト可能性を向上させる事です。
DIPとは
マーティンファウラーはオブジェクト指向プログラミングのガイドラインとして、S.O.L.I.D.5原則を提唱しました。その中の1つが 依存性逆転の原則、DIP (dependency inversion principle)です。
何が逆転するのでしょうか?以下の2つのルールがあります。
- A. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。
- B. 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。
例えばアプリケーションコードがWeb APIのリクエストをGuzzle httpクライアントで行う事を考えましょう。この時、アプリケーションは「上位レベルのモジュール」でGuzzle APIクライアントは 「下位レベルのモジュール」です。
つまり、コードが直接Guzzleを扱うことは「上位レベルのモジュールは下位レベルのモジュールに依存すべきではない」のDIPに違反します。
アプリケーションは直接Guzzleを使う代わりに、アプリケーションにとって必要な「アプリケーション専用のWeb APIインターフェイス」を作成し、そのインターフェイスを使うだけにします。インターフェイスに対して実クラスのGuzzleを使ったクラスを作成します。これがBの「詳細が抽象に依存」です。
例えとして、電化製品が電力を使用する場合に、実際の電力機構には興味を示さずコンセントの規格だけに依存します。電力機構も利用するコンセントというインターフェイスだけに依存します。どちらも抽象に依存する事で疎結合にし、メンテナンス性や利用性が向上します。
DIを使い、DIP原則を守ることができます。
DIコンテナ → インジェクター
DIコンテナは依存が格納されたコンテナ(容器)です。多くのDIツールではコンテナに依存をセットする事で設定を行いますが、Ray.Di (Guice)は依存の生成時にはもちろん、設定時にもコンテナを扱いません、(マニュアルにもコンテナという用語も出てきません)コンテナはソフトウエア奥に隠され、UIとして現れることはありません。Ray.Di(Guice)ではコンテナの代わりにインジェクターという言葉が使われます。
サービスロケーターとの違い
依存の取得時にDIコンテナをユーザーが直接扱うのがサービスロケーターです。DIと同じようにソフトウエアパターンの名前です。それに対してクラスは渡される(注入される)事だけを期待するのがDIです。
例えばSlimやSymfonyでコントローラーにコンテナが渡されている事があります(またはありました)が、それはDIではではなくサービスロケーター(SL)です。DIではコンテナに直接アクセスしません。
SLは一般にアンチパターンだとされます。オブジェクト間の結合度を上げてしまい、テストをより難しいものにします。より詳しくはDIコンテナとサービスロケータを区別する方法についての記事をご覧ください。
なぜRay.Diを使うのか?
- Ray.Diはオブジェクトの生成と利用を分離します。分離されることで...
- 利用コードはよりシンプルになります。
- より変化に強いコードになります。
- テストのしやすいコードになります。
- コンテキキストに応じた束縛もでき再利用性が高まります。
- AOPもできます。
AOPとは
DIを補完するため、Ray.Diはメソッドのインターセプトをサポートしています。
特定のメソッドを"検索"して、一致するメソッドが呼び出されるたびに実行されるコードを書くことができます。トランザクション、セキュリティ、ロギングなどの横断的な問題(アスペクト)に適しています。インターセプターは問題をオブジェクトではなくアスペクトに分割するので、その使用はアスペクト指向プログラミング(AOP)と呼ばれています。
AOPは通常のコーディングでは難しい、横断的な関心事と本質的関心事の分離ができます。クラスマッチャー、メソッドマッチャーでは名前やアトリビュート(アノテーション)に対して、例えば"delete"ではじまるとか"@Loggable"というアノテーシュンがついてるなどといった条件とそれに適用するメソッドインターセプターを指定します。
メソッドインターセプターではそのメソッドの実行オブジェクトを受け取りその前後の処理を記述することによってメソッドで行われる前後の処理を記述する事ができます。
Ray.Di 特徴
- アトリビュート (PHP7ではdoctrineアノテーション )サポート
- 基本が自動束縛 (auto wiring)
- 同じインターフェイスでも使い分ける識別子(Qualifier)
- シリアライズ、コンパイル可能な高速コンテナ
- AOPとの統合
- JSR330アノテーション
- メソッドインジェクション
- 後方互換を破壊しない長期サポート
- CDI (実行コンテキストによるDI)
サポートするバージョン
- サポートされているPHPのバージョンをサポートします。
束縛
Ray.Diでは依存を実装に束縛することが基本です。
基本は"インターフェイス"に"クラス"をインターフェイスや抽象クラスに具象クラスを束縛するのが基本です。これをリンク束縛と言います。
interface FinderInterface
{
}
class Finder implements FinderInterface
{
}
モジュールで束縛します。
class FinderModule extends AbstractModule
{
protected function configure()
{
$this->bind(FinderInterface::class)->to(Finder::class);
}
}
束縛の情報をもったモジュールをインジェクターに渡し、インスタンスを生成します。
$injector = new Injector(new FinderModule());
$movieLister = $injector->getInstance(MovieListerInterface::class);
assert($movieLister->finder instanceof Finder);
-
依存が他の依存を持つかもしれません。その場合でも循環して依存解決(dependency resolution)されます。
-
モジュールはパフォーマンスのためにシリアライズ可能です。
名前束縛
同じインターフェイスでも振る舞いの違う実装を束縛したい時があります。これをロボットの足問題と言います。("足"というインターフェイスは同じでも右足と左足で"実装"は違っています)この問題を解決するのが名前束縛です。
@Named
を使い変数名=識別子として指定し識別します。
/** @Named(rightLeg=right, leftLeg=left) */
public function __construct(LegInterface $rightLeg, LegInterface $leftLeg)
{
}
PHP8なら引数にアトリビュートを記述でき、コンストラクタプロモーション も利用でき、このような記述になります。
public function __construct(
#[Named('right') private LegInterface $rightLeg,
#[Named('left') private LegInterface $leftLeg
){}
束縛はこのようにannotatedWith
を用います。
protected function configure()
{
$this->bind(LegInterface::class)->annotatedWith('right')->to(RightLeg::class);
$this->bind(LegInterface::class)->annotatedWith('left')->to(LeftLeg::class);
}
識別はNamed
で簡易に利用できますが、型の指定できないスカラー値などはNamed
ではなく専用の識別子を作成します。namespaceを利用し、他とコンフリクトすることがありません。
識別子 RightLeg
/**
* @Annotation
* @Target("METHOD")
*/
#[Attribute(Attribute::TARGET_PARAMETER)]
class RightLeg
{
}
protected function configure()
{
$this->bind(LegInterface::class)->annotatedWith(RightLeg::class)->to(RightLeg::class);
$this->bind(LegInterface::class)->annotatedWith(RightLeg::class)->to(LeftLeg::class);
}
プロバイダー束縛
依存を返すファクトリーコードをプロバイダーと呼びます。その束縛がプロバイダー束縛です。プロバイダーもDIされるので他の依存を利用することができます。
その他の束縛
- バリューオブジェクトなど直接値を束縛するインスタンス束縛
- インターフェイスのないクラスを直接束縛する、アンターゲット束縛
- アノテーション(アトリビュート)のないクラスを対象とするコンストラクタ束縛
- インターフェイスをNullオブジェクトと束縛するNull束縛
アシスティッドインジェクション
-
メソッド実行時にDIを行うアシスティッドインジェクションはDIの時に他のパラメーターの値を参照することができます。
コンパイラ
- Ray.Diは束縛のルールをPHPの実行コードにコンパイルすることができます。速度は"これ以上は速くなれないレベル"でインジェクションを実行します。
他のDIコンテナとの違い、優位点
自動束縛
PHPには多くのDIコンテナがありますが、以下の特徴を持つものがあります。
- コンテナに対してサービスをキーまたはクラス名でユーザーが登録する
- 「コンテナが渡された無名関数内」で
new
して依存を渡す。 - 変数名をグローバルな識別子と扱いその依存を渡す。
- 依存を必要とするクラスに応じて依存を選択して渡す。
1,2をベースとしてインターフェイスとクラスを束縛するauto wiringがオプションで用意されているDIコンテナもいくつかあります。Ray.Diはこれらの特徴をどれも採用していません。
Ray.Diは基本的には、オブジェクトをどう組み立てるか手順を教えるのではなく、どの依存とどの実装が束縛するのかという束縛ルールだけでオブジェクトが組み立てられます。束縛のルールが変わらない限り、新しいクラスの利用に対して新しい設定記述の必要はありません。依存の種類が増えない限り、エンジニアにRay.Diでの設定知識は必要ありません。
識別子
- 変数名で識別するのではなく、依存に対して識別子を与えて依存を区別します。ロボットの足問題を解決します。識別子のないDIコンテナでは変数名を識別子に代用するDIコンテナもありますが、それよりずっとクリーンな方法です。
パフォーマンス
- 無名関数を使うとシリアライズが不可能になり、毎リクエストに全く同じ束縛をコンテナにセットする必要があります。Ray.Diのコンテナはシリアライズ可能です。またコンパイラを使えばPHPのファクトリーコードのスクリプトを生成し、これ以上高速にできないレベルで依存解決ができます。
- コンテナを1つの大きな変数にしないため、束縛の数が増えてもプロダクションでは使用していない束縛に対してメモリ使用量が増えません。それぞれの依存解決は別ファイルとしてコンパイルされるため、極端に多い束縛数でもコンパイルされたプロダクション環境では影響がありません。
長期サポート
- 機能拡張とリファクタリング、最新のPHPをサポートし続けていますが2011年の開発当初からメインの箇所は変更がありません、2015年のv2からは完全な後方互換性を維持しています。今後破壊する予定(version 3)の予定もありません。
- 10年経って機能は増えていますが、互換性は維持しつつコードはよりクリーンになっています。これは静的解析ツールなどツールの進化の影響もあります。
AOPサポート
- AOPと統合されたDIシステムはPHPではRay.Diだけです。AOPはOOPで解決できない問題を上手く解決します。横断的処理をチームのフレームワークにすることもできます。