依赖注入:在实际需要时拉出所需的组件

时间:2012-05-14 18:03:56

标签: php design-patterns dependency-injection

DI背后的要点是减轻一个类创建和准备它所依赖的对象并将它们推入。这听起来很合理,但有时候一个类不需要所有的对象,它们被推入其中来执行它的功能。这背后的原因是"早期回归"在无效的用户输入或之前所需的一个对象抛出的异常或在代码块运行之前实例化对象所必需的某个值不可用时发生的情况。

更实际的例子:

  • 注入永远不会使用的数据库连接对象,因为用户数据未通过验证(前提是没有触发器用于验证此数据)
  • 注入收集输入的类似excel的对象(例如PHPExcel)(因为整个库被拉入并且从未使用过,因此加载和实例化很重,因为验证会在写入发生之前抛出异常)
  • 在类中确定的变量值,但在运行时不是注入器;例如,一个路由组件,用于确定应根据用户输入调用的控制器(或命令)类和方法
  • 虽然这可能是一个设计问题,但是一个实质性的服务类,它依赖于很多组件,但每个请求只使用1/3(原因,为什么我倾向于使用命令类而不是控制器)

因此,在某种程度上推动所有必要的组件矛盾和延迟加载"在创建和从未使用某些组件的方式中,这有点不实用且影响性能。就PHP而言 - 加载,解析和编译更多文件。如果被推入的对象具有自己的依赖关系,那么这尤其痛苦。

我看到它有3种方法,其中2种听起来不太好:

  • 注射工厂
  • 注入注射器(反模式)
  • 注入一些外部函数,从内部调用 一旦达到一个相关的点(smtg喜欢"检索一个 PHPExcel实例一旦数据验证完成");这就是我 由于其灵活性倾向于使用

问题是处理这种情况的最佳方法是什么/你们用什么?

更新: @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 个答案:

答案 0 :(得分:7)

我最近在计划一个我想尽可能正确的重大项目时一直在考虑这个问题(坚持LoD,没有硬编码依赖等)。我的第一个想法是“注入工厂”的方法,但我不确定这是要走的路。谷歌的清洁代码谈话声称,如果你通过一个对象来获取你真正想要的对象,那么你就违反了LoD。这似乎排除了注入工厂的想法,因为你必须通过工厂来获得你真正想要的东西。也许我错过了一些让它没问题的观点,但直到我确定我正在考虑其他方法。

你如何进行功能注射?我想你会传入一个回调函数来实现你想要的对象的实例化,但代码示例会很好。

如果您可以使用代码示例更新您的问题,例如您提到的三种样式可能会有用。我特别渴望看到“注射注射器”,即使它是反模式。

答案 1 :(得分:5)

确实发生的一个想法是代理对象。它实现了与您想要传入的实际对象相同的接口,但它不是实现任何东西,而是只保存真实类的实例并将方法调用转发给它。

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替换原则适用于代理,因为它响应所有与真实类相同的消息并返回相同的结果(接口强制执行此操作,至少对于方法签名)。但是,除非实际将消息发送到代理,否则实际类不会被实例化,这会在后台对实际类进行惰性实例化。

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

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

代理对象有一个额外的间接层,这显然意味着每个方法调用都会产生很小的性能影响。但是,它确实允许您进行延迟实例化,因此您可以避免实例化您不需要的类。这是否值得,取决于实例化实际对象的成本与额外的间接层的成本。

编辑:我最初编写这个答案的想法是对真实对象进行子类化,因此您可以将该技术用于不实现任何接口(如PDO)的对象。我原本以为接口是正确的方法,但我想要一种不依赖于类与接口绑定的方法。在反思这是一个很大的错误,所以我更新了答案,以反映我本来应该做的事情。但是,此版本的确意味着您无法直接将此技术应用于没有关联接口的类。您必须将这些类包装在另一个类中,该类确实为代理方法提供了可行的接口,这意味着另一层间接。

答案 2 :(得分:3)

如果你想实现延迟加载,你基本上有两种方法可以做到这一点(正如你已经在主题中写的那样):

  1. 而不是注入可能需要的对象实例,而是注入FactoryBuilder。它们之间的区别在于Builder的实例用于返回一种类型的对象(可能具有不同的设置),而Factory生成不同类型的实例(具有相同的生命周期和/或实现相同的接口)

  2. 利用匿名函数返回实例。这看起来像这样:

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

    只有在调用此匿名函数时,才会创建PDO的实例并建立与数据库的连接。

  3. 我通常在代码中执行的操作是合并。您可以为Factory装备provider。例如,当您第一次询问来自Factory的实例时,这允许您为由所述工厂创建的所有对象建立单个连接,并且仅创建连接。

    组合两种方法的另一种方法(我还没有使用过)可以创建完整的Provider类,它在构造函数中接受匿名函数。然后工厂可以传递同一个Provider实例,昂贵的对象(PHPExcel,Doctrine,SwiftMailer或其他一些实例)仅在第ProductFactory Provider时创建到Products (无法用更好的名称来描述同一工厂创建的所有对象)并请求它。之后,这个昂贵的对象在所有Factory {{1}}之间共享。

    ...我的2美分

答案 3 :(得分:0)

我选择了 lazy-injection (即注入一个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 (依赖注入框架)中实现。

免责声明:我在这个项目中工作