ただし、PHPではめったに見られないアプローチが1つあります。 これには、ネイティブ継承の使用が含まれており、コードのパッチを「改善」することができます(c)。 このメソッドを「Forwarding Decorator」と呼びます。 私たちには非常に効果的であるように思われますが、生産的にはそれほど重要ではありませんが、ところで、壮観でもあります。
SitePointで公開された元の英語の記事「 Forwarding Decoratorsを使用したモジュラーアーキテクチャの作成 」の著者として、著者の翻訳版を紹介します。 その中で、私はもともと考えられていた意味とアイデアを保持しましたが、フローを最大化しようとしました。
はじめに
この記事では、Forwarding Decoratorを使用したアプローチの実装と、その長所と短所について検討します。 このアプローチを、フック、コードパッチ、またはDI(依存性注入)を使用する他のよく知られている選択肢と比較してください。 わかりやすくするために、このGitHubリポジトリにデモアプリケーションがあります 。
主なアイデアは、各クラスをサービスと見なし、コードをコンパイルするときに継承者のチェーンを継承および反転することでこのサービスを変更することです。
このような考えに基づいたシステムでは、どのモジュールでも特別なデコレータークラス(特別な方法でマークされた)を作成できます。 このようなクラスは、継承メカニズムを介して別のクラスのフィールドとメソッドを受け取りますが、コンパイル後は元のクラスの代わりにどこでも使用されます。
実際、そのようなクラスをフォワーディングデコレーターと呼んでいるのはこのためです。これらのデコレーターは元の実装の上位構造ですが、使用する場所で前方に移動します。
このアプローチの利点は明らかです。
- システムの任意の部分は、モジュールを使用して拡張できます-任意のクラス、任意のpublic / protectedメソッド。 事前に特別なコードで拡張ポイントをマークする必要はありません。
- 1つのサブシステムは、複数のモジュールによって同時に変更できます。
- サブシステムは弱く相互接続されているため、互いに独立して個別に更新できます。
- プライベートメソッドと閉じた(最終的な)クラスというよく知られた構造を使用して、拡張性を制限できます。
ただし、このアプローチには欠点もあります。
- まず第一に、これは拡張可能なシステムとの明確な相互作用インターフェースの欠如です。 privateを介して明示的に禁止されていないすべてを拡張できますが、システムは間違った目的から入力されたとは思わないかもしれず、モジュール開発者が考えなかった場合には不適切に動作します。 望ましくない副作用がないか、コードを注意深く調べる必要があります。
- ある種のコンパイラを実装する必要があります(詳細は以下)。
- モジュールを開発するときは、サブシステムのパブリックインターフェイスを厳密に観察し、 Lisk置換の原則に違反しないようにする必要があります。そうしないと、これらのモジュールはシステムを破壊します。
- コンパイラを追加すると、コードのデバッグが複雑になります。 ソースコードでXDebugを直接実行することはできません;コードを変更するには、コンパイラを最初に起動する必要があります。 ただし、コンパイルされたファイルが起動されるようにトリッキーなPHPトリックを使用することでこの問題を解決できますが、同時にデバッガーにソースコードが表示されます。
システムを拡張する同様の方法は、ある意味で、直接的なコードパッチ(低レベル、ゲームルールなし、ゴッドモード、最大のパワー、ただし最大の責任など)とプラグインアーキテクチャの中間的なソリューションです。サブシステムとその変更方法が提供するプラグインがあるかもしれません。 デコレータシステムを使用すると、特定の範囲の問題を適切に解決できますが、これは特効薬ではなく、モジュール性を整理する理想的な方法ではありません。
このようなシステムを使用するにはどうすればよいですか?
以下に例を示します。
class Foo { public function bar() { echo 'baz'; } }
namespace Module1; /** * , DecoratorInterface ( : , ) */ class ModifiedFoo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } }
// ... - $object = new Foo(); $object->bar(); // will echo 'baz modified'
どうして起こったの? これはストリートマジックです)継承のチェーンを元に戻しています。 ソースクラスには内部コードがありません。 コンパイルの結果として、ソースクラスのコンテンツがチェーンの新しい親になる別のクラスに入るように、コードを前処理します。
// , , class Foo extends \Module1\ModifiedFoo { // move the implementation from here to FooOriginal }
namespace Module1; // , abstract class ModifiedFoo extends \FooOriginal implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } }
// . class FooOriginal { public function bar() { echo 'baz'; } }
要するに、コンパイラは、中間クラスを構築するアプリケーションと、元のクラスの代わりにこれらの中間クラスをロードするオートローダーに組み込まれています。
そしてもう少し。 コンパイラーは、システムで使用されるすべてのクラスのリストを作成し、デコレーターではない各クラスについて、 DecoratorInterfaceを使用してそれを装飾するすべてのサブクラスを見つけます。 彼はデコレーターのツリーを作成し、ループがあるかどうかを確認し、優先順位に従ってデコレーターをソートします(詳細については後で説明します)。継承チェーンが反対方向にデプロイされる中間クラスを構築します。 ソースコードは新しいクラスに変換され、継承チェーンの新しい親クラスになります。
複雑に聞こえます。 つまり、本当に複雑で複雑なシステムです。 ただし、モジュールを非常に柔軟に組み合わせることができ、これらのモジュールを使用すると、アプリケーションの任意の部分を完全に変更できます。
そして、1つのクラスが複数のモジュールによって上書きされた場合はどうなりますか?
複数のデコレータが同時にゲームに参加すると、優先度に従ってデコレーションチェーンに分類されます。 優先度はアノテーション(Doctrine \ Annotationsを使用)または設定を使用して設定できます。
例を考えてみましょう:
class Foo { public function bar() { echo 'baz'; } }
namespace Module1; class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } }
namespace Module2; /** * @Decorator\After("Module1") */ class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' twice'; } }
// ... - $object = new Foo(); $object->bar(); // 'baz modified twice'
この例では、 デコレータ\ Afterアノテーションを使用して、モジュール2の前に別のモジュール1のデコレータを配置します。コンパイラはファイルを分析し、アノテーションを考慮して、次の継承チェーンを持つ中間クラスを構築します。
次の注釈も使用できます。
- デコレータ\前 (デコレータを他のモジュール以上の重量のデコレータの前に配置するため)
- デコレータ\依存 (指定されたモジュールがシステムで有効になっている場合にのみデコレータを有効にします)
この一連の注釈(Before、After、Depend)は、モジュールとクラスの任意の組み合わせを構築するのに絶対に十分です。
実用的な例はありますか?
あります! 明確にするために、このGitHubリポジトリにあるアプリケーションのデモを用意しました。 このPHPで作成されたアプリケーションにはモジュール式のアーキテクチャがあり、モジュールは再コンパイルせずにコードを混在させることができます。 同時に、モジュールを追加および削除できますが、この場合、再コンパイルがすでに必要です。 これらはすべて、 readmeファイルで詳細に説明されています。
かなり「戦闘」の例があります。 このアプローチを使用するソフトウェア製品がすでにいくつか市場に出回っています。 特に、OXID eShopでは非常によく似たものが使用されます。 ところで、彼らはクールなブログ投稿スタイルを持っています。 X-Cart 5の別のプラットフォームでは、このアプローチは、私が説明した形式で正確に実装されています-X-Cart 5コードは、この記事の基礎としても採用されました。 これにより、開発者の想像力(または顧客のお金=)だけ拡張でき、その後のカーネルアップグレードを中断することなく、eコマース用の非常に柔軟なソリューションを作成できました。
使い慣れたフックとパッチの方が優れています! かどうか?
Forwarding Decoratorsのアプローチと同様に、フックと前面パッチの使用には長所と短所があります。
- フック (またはObserverパターンの実装)は、Wordpressなどの多くの一般的なアプリケーションで広く使用されています。 利点の中には、明確に定義されたAPIがあり、オブザーバーを透過的に登録できます。 最大の欠点は、拡張機能を組み込むためのエントリポイントの数が限られていることであり、実行順序も不便です(他のフックの結果に依存することは困難です)
- 「ヘッドオン」パッチは、拡張するための最も簡単で明白な方法ですが、私たちにはかなり危険です。 第一に、コードの読み取りと分析が大幅に複雑になり、第二に、変更が正しくない場合に変更のロールバックが複雑になります。 また、複数のパッチを同時に適用して、互いに矛盾せず、機能を損なわないようにするのは複雑です。 言い換えれば、これは最も制御が少なく管理しやすい方法であり、単純なソリューションでそれを正当化する場合、システムの複雑さにより、これらのマイナスはその複雑さに比例して大きくなります。
- 依存性注入-DIを使用したシステムのコードは、必要な依存関係は手動で取得されず、外部から配信されるか間接的にアクセスされるという理解に基づいて構築されます-再び特定のプロバイダー(ほとんどの場合、これは何らかのIoCコンテナーです)
依存関係は特定のインターフェイスを満たし、特定の機能の完全な実装です。 拡張システムにより、システムの現在の構成に基づいて、依存関係の実装を別のものに置き換えることができます。
実装は、たとえばSymfony 2のように、 ここで説明するように 、ベースから継承するか、デコレータの古典的な意味で装飾することができます 。 このアーキテクチャの問題は、依存関係を取得するDIスタイルを使用してすべてのコードを構築する必要があることです。 この記事で説明されているシステムとの違いは、転送デコレーターを使用すると、すべての使用ポイントでクラスを完全に透過的に置換できることです。
さらに、同じサービスを拡張する複数のモジュールの構成を整理する方法は不明です-人気のあるIoCコンテナーはこの問題を解決しないため、別のシステムを作成する必要があります(これはこのようなライブラリの範囲外です)。
おわりに
転送デコレータは、少なくとも注目に値するアプローチです。 PHPで拡張可能なモジュラーアプリケーションアーキテクチャを開発する問題を解決するために使用できます。 これは、継承やフィールド/メソッド/クラスのスコープなどのよく知られた構造を使用します。
このような概念を実装するのは簡単な作業ではなく、デバッグには困難があるかもしれませんが、コンパイラーを適切に構成するのにある程度の時間を費やせば克服できます。
この資料に興味がある場合は、次の記事で、オートローダーで最適なコンパイラーを作成し、ストリームフィルター(PHPストリームフィルター)を使用して、XDebugによるソースコードの段階的なデバッグを有効にする方法を説明します。 面白い? コメントで教えてください。 そして、私はあなたの質問、アドバイス、建設的な批判に喜んでいます。