18

DI の背後にある要点は、クラスが依存するオブジェクトを作成して準備し、それらをプッシュすることからクラスを解放することです。これは非常に合理的に聞こえますが、クラスは、その機能を実行するためにクラスにプッシュされているすべてのオブジェクトを必要としない場合があります。この背後にある理由は、無効なユーザー入力、必要なオブジェクトの 1 つによって以前にスローされた例外、またはコード ブロックが実行されるまでオブジェクトをインスタンス化するために必要な特定の値が利用できない場合に発生する「アーリー リターン」です。

より実用的な例:

  • ユーザーデータが検証に合格しないため、使用されることのないデータベース接続オブジェクトを挿入します (このデータを検証するためにトリガーが使用されていない場合)。
  • 入力を収集する Excel のようなオブジェクト (PHPExcel など) を挿入する (ライブラリ全体が取り込まれて使用されないため、読み込みとインスタンス化が重くなります。書き込みが発生する前に検証が例外をスローするためです)。
  • 実行時にインジェクターではなく、クラス内で決定される変数値。たとえば、ユーザー入力に基づいて呼び出されるコントローラー (またはコマンド) クラスとメソッドを決定するルーティング コンポーネント
  • これは設計上の問題かもしれませんが、かなりのサービスクラスであり、多くのコンポーネントに依存しますが、リクエストごとにそれらの 1/3 程度しか使用しません (理由は、コントローラーの代わりにコマンドクラスを使用する傾向があるためです)。

したがって、必要なすべてのコンポーネントをプッシュすることは、一部のコンポーネントが作成されて使用されないという点で「遅延読み込み」と矛盾します。これは少し非現実的で、パフォーマンスに影響を与えます。PHP に関する限り、より多くのファイルがロード、解析、およびコンパイルされます。プッシュされるオブジェクトに独自の依存関係がある場合、これは特に苦痛です。

私はそれを回避する方法を 3 つ見ていますが、そのうちの 2 つはうまく聞こえません。

  • 工場への注入
  • インジェクターの注入 (アンチパターン)
  • 関連するポイントに到達すると、クラス内から呼び出される外部関数を注入します (「データ検証が終了したら、PHPExcel インスタンスを取得する」などの smtg)。これは、その柔軟性のために私が使用する傾向があるものです

問題は、そのような状況に対処する最善の方法は何ですか / 皆さんは何を使っていますか?

更新: @GordonM ここに 3 つのアプローチの例があります。

//inject factory example
interface IFactory{
    function factory();
}
class Bartender{
    protected $_factory;

    public function __construct(IFactory $f){
        $this->_factory = $f;
    }
    public function order($data){
        //validating $data
        //... return or throw exception
        //validation passed, order must be saved
        $db = $this->_factory->factory(); //! factory instance * num necessary components
        $db->insert('orders', $data);
        //...
    }
}

/*
inject provider example
assuming that the provider prepares necessary objects
(i.e. injects their dependencies as well)
*/
interface IProvider{
    function get($uid);
}
class Router{
    protected $_provider;

    public function __construct(IProvider $p){
        $this->_provider = $p;
    }
    public function route($str){
        //... match $str against routes to resolve class and method
        $inst = $this->_provider->get($class);
        //...
    }
}

//inject callback (old fashion way)
class MyProvider{
    protected $_db;
    public function getDb(){
        $this->_db = $this->_db ? $this->_db : new mysqli();
        return $this->_db;
    }
}
class Bartender{
    protected $_db;

    public function __construct(array $callback){
        $this->_db = $callback;
    }
    public function order($data){
        //validating $data
        //... return or throw exception
        //validation passed, order must be saved
        $db = call_user_func_array($this->_db, array());
        $db->insert('orders', $data);
        //...
    }
}
//the way it works under the hood:
$provider = new MyProvider();
$db = array($provider, 'getDb');
new Bartender($db);

//inject callback (the PHP 5.3 way)
class Bartender{
    protected $_db;

    public function __construct(Closure $callback){
        $this->_db = $callback;
    }
    public function order($data){
        //validating $data
        //... return or throw exception
        //validation passed, order must be saved
        $db = call_user_func_array($this->_db, array());
        $db->insert('orders', $data);
        //...
    }
}
//the way it works under the hood:
static $conn = null;
$db = function() use ($conn){
    $conn = $conn ? $conn : new mysqli();
    return $conn;
};
new Bartender($db);
4

4 に答える 4

7

私は最近、人間的に可能な限り正しく実行したい主要なプロジェクトを計画する際に、この問題についてよく考えています (LoD に固執し、依存関係をハードコーディングしないなど)。私が最初に考えたのは、「工場を注入する」アプローチでもありましたが、それが正しいかどうかはわかりません。Google の Clean Code トークでは、本当に必要なオブジェクトを取得するためにオブジェクトを介してアクセスする場合、LoD に違反していると主張しています。本当に欲しいものを手に入れるには工場を通り抜けなければならないので、それは工場を注入するという考えを排除するように思われる. 多分私はそれを大丈夫にするいくつかのポイントを逃したかもしれませんが、私が確実に知るまで、私は他のアプローチを熟考しています.

関数注入はどのように行うのですか?必要なオブジェクトのインスタンス化を行うコールバックを渡していると思いますが、コード例があればいいでしょう。

あなたが言及した3つのスタイルをどのように行うかのコード例で質問を更新できれば、それは役に立つかもしれません. たとえそれがアンチパターンであっても、「インジェクターを注入する」ことに特に熱心です。

于 2012-05-14T19:26:11.070 に答える
5

実現した 1 つのアイデアは、プロキシ オブジェクトのアイデアでした。渡したい実際のオブジェクトと同じインターフェイスを実装しますが、何かを実装する代わりに、実際のクラスのインスタンスを保持し、メソッド呼び出しを転送します。

interface MyInterface 
{
    public function doFoo ();
    public function isFoo ();
    // etc
}

class RealClass implements MyInterface
{
    public function doFoo ()
    {
         return ('Foo!');
    }

    public function isFoo ()
    {
        return ($this -> doFoo () == 'Foo!'? true: false);
    }

    // etc
}

class RealClassProxy implements MyInterface
{
    private $instance = NULL;

    /**
     * Do lazy instantiation of the real class
     *
     * @return RealClass
     */
    private function getRealClass ()
    {
        if ($this -> instance === NULL)
        {
            $this -> instance = new RealClass ();
        }
        return $this -> instance;
    }

    public function doFoo ()
    {
        return $this -> getRealClass () -> doFoo ();
    }

    public function isFoo ()
    {
        return $this -> getRealClass () -> isFoo ();
    }

    // etc
}

プロキシには実際のクラスと同じインターフェイスがあるため、インターフェイスのヒントを入力する任意の関数/メソッドに引数として渡すことができます。Liskov Substitution Principle は、実際のクラスと同じすべてのメッセージに応答し、同じ結果を返すため、プロキシにも当てはまります (インターフェイスは、少なくともメソッド シグネチャに対してこれを強制します)。ただし、メッセージが実際にプロキシに送信されない限り、実際のクラスはインスタンス化されません。プロキシは、舞台裏で実際のクラスの遅延インスタンス化を行います。

function sendMessageToRealClass (MyInterface $instance)
{
    $instance -> doFoo ();
}

sendMessageToRealClass (new RealClass ());
sendMessageToRealClass (new RealClassProxy ());

プロキシ オブジェクトに関連する追加の間接レイヤーがあります。これは明らかに、実行するすべてのメソッド呼び出しに対してわずかなパフォーマンス ヒットがあることを意味します。ただし、遅延インスタンス化を行うことができるため、不要なクラスのインスタンス化を回避できます。これが価値があるかどうかは、実際のオブジェクトをインスタンス化するコストと、追加の間接レイヤーのコストに依存します。

編集:私はもともと、実際のオブジェクトをサブクラス化するという考えでこの回答を書いていたので、PDO などのインターフェイスを実装しないオブジェクトでこの手法を使用できます。私は当初、これを行うにはインターフェイスが正しい方法だと考えていましたが、インターフェイスに関連付けられているクラスに依存しないアプローチが必要でした。これは大きな間違いだったので、最初にすべきことを反映するように回答を更新しました。ただし、このバージョンでは、関連付けられたインターフェイスを持たないクラスにこの手法を直接適用することはできません。そのようなクラスを、プロキシ アプローチを実行可能にするためのインターフェイスを提供する別のクラスでラップする必要があります。つまり、間接的な別のレイヤーを意味します。

于 2012-05-17T13:11:24.140 に答える
3

遅延読み込みを実装する場合は、基本的に 2 つの方法があります (トピックで既に説明したように)。

  1. 必要なオブジェクトのインスタンスを注入する代わりに、またはFactory注入しますBuilder。それらの違いは、 のインスタンスがBuilder1 つのタイプのオブジェクトを返すために作成される (おそらく異なる設定で) のに対しFactory、異なるタイプのインスタンスを作成する (同じ有効期間および/または同じインターフェイスを実装する) ことです。

  2. インスタンスを返す匿名関数を利用します。それは次のようになります。

    $provider = function() {
        return new \PDO('sqlite::memory:');
    };
    

    この無名関数を呼び出した場合にのみ、 のインスタンスPDOが作成され、データベースへの接続が確立されます。

コードで通常行うことは、両方を組み合わせることです。Factoryなどを装備することができますprovider。これにより、たとえば、上記のファクトリによって作成されたすべてのオブジェクトに対して単一の接続が可能になり、最初に からインスタンスを要求したときにのみ接続が作成されますFactory

両方のメソッドを組み合わせるもう1つの方法(ただし、使用していません)はProvider、コンストラクターで匿名関数を受け入れる本格的なクラスを作成することです。次に、ファクトリはこの同じインスタンスを渡すことができProvider、高価なオブジェクト (PHPExcel、Doctrine、SwiftMailer、またはその他のインスタンス) はProduct、そのFactory最初の時点から一度だけ作成されますProvider (すべてのオブジェクトを説明するためのより良い名前を思い付くことができませんでした)同じ工場で作成された)とそれを要求します。その後、この高価なオブジェクトはすべてProductsの間で共有されますFactory

...私の2セント

于 2012-05-17T13:44:36.400 に答える
0

レイジーインジェクション(つまり、Proxyクラスのインジェクション)を選択しました。

class Class1 {
    /**
     * @Inject(lazy=true)
     * @var Class2
     */
    private $class2;

    public function doSomething() {
        // The dependency is loaded NOW
        return $this->class2->getSomethingElse();
    }

ここでは、依存関係(class2)は直接注入されません。プロキシクラスが注入されます。プロキシクラスが使用されている場合にのみ、依存関係が読み込まれます。

これは、PHP-DI(依存性注入フレームワーク)で可能です。

免責事項:私はこのプロジェクトで働いています

于 2012-10-07T09:32:51.723 に答える