【问题标题】:Dependency Injection: pulling required components when they are actually needed依赖注入:在实际需要时拉取所需的组件
【发布时间】:2012-05-14 18:03:55
【问题描述】:

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);

【问题讨论】:

  • 这是一个非常好的问题,但与 Stack Overflow 相比,它似乎更适合程序员。
  • 任何提示我如何将它转移到那里? O-)
  • 您是否在寻找延迟加载/初始化? Advanced OO Patterns (video)

标签: php design-patterns dependency-injection


【解决方案1】:

我最近在规划一个重大项目时一直在考虑这个问题,我想尽可能正确地完成这个项目(坚持 LoD,没有硬编码的依赖项等)。我的第一个想法也是“注入工厂”方法,但我不确定这是要走的路。来自 Google 的 Clean Code 会谈声称,如果您通过一个对象到达您真正想要的对象,那么您就违反了 LoD。这似乎排除了注入工厂的想法,因为你必须通过工厂才能得到你真正想要的东西。也许我错过了一些让它变得好的点,但直到我确定我正在考虑其他方法。

函数注入是怎么做的?我想你正在传递一个回调来实例化你想要的对象,但是一个代码示例会很好。

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

【讨论】:

  • 通过... - 是的;大的想法是类应该是“浅的”并且只引用其他相关的对象; singleton、injector 和 factory 不是相关参考; 你如何进行函数注入? - 我有一个特殊的Callback 类,我将它作为回调传递(因为 PHP 中没有内部类);随着 PHP 5.3 的出现,现在可以使用 lambda 函数;我很快就会举一些例子
  • @GrigorashVasilij 你知道,如果这个问题转移到programmers.stackexchange.com 可能会更好,因为它的术语非常抽象,而stackoverflow 真的应该用于解决更具体的问题(我的实施不起作用,你能帮忙吗?)。你(和我!)可能会在那里得到一些有用的答案。
  • 如果你注入了一个工厂,你并没有违反 LoD。参见 LoD 上的原始论文:ccs.neu.edu/research/demeter/papers/law-of-demeter/…
【解决方案2】:

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

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)的对象。我最初认为接口是执行此操作的正确方法,但我想要一种不依赖于绑定到接口的类的方法。经过反思,这是一个很大的错误,所以我更新了答案以反映我首先应该做的事情。但是,此版本确实意味着您不能直接将此技术应用于没有关联接口的类。您必须将此类类包装在另一个类中,该类确实为代理方法提供了一个接口,这意味着另一个间接层。

【讨论】:

  • 恕我直言,使用代理您还可以创建和注入代理对象(而不是实际对象);所以,比如说,如果你有 3 个必要的组件,你也会有 3 个代理创建 3 个实际对象;如果实际对象实例化很重,那么这种方法很好;但如果不是这样的话——那就没什么意义了
【解决方案3】:

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

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

  2. 使用匿名函数,它会返回一个实例。看起来像这样:

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

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

我通常在我的代码中做的是结合两者。您可以为Factory 配备这样的provider。例如,这使您可以为由所述工厂创建的所有对象建立一个单一连接,并且仅当您第一次从 Factory 询问实例时才创建该连接。

结合这两种方法的另一种方法(虽然我没有使用过)是创建完整的Provider 类,它在构造函数中接受一个匿名函数。然后工厂可以传递Provider 的同一个实例,并且昂贵的对象(PHPExcel、Doctrine、SwiftMailer 或其他实例)仅在ProductFactory 第一次转到Provider 时才被创建。 em>(无法想出更好的名称来描述由同一工厂创建的所有对象) 并请求它。之后,这个昂贵的对象在所有ProductsFactory 之间共享。

...我的 2 美分

【讨论】:

  • 这种方法对我来说似乎有问题,因为它使依赖类依赖于工厂/构建器/回调而不是它真正依赖的类。这意味着依赖类真正依赖的内容不太明显。进行清洁代码演讲的人指出,“如果你付钱,你是把钱交给店员,还是把钱包交给店员,让店员取出所需的金额?” youtu.be/RlfLCWKxHJ0
  • @GordonM :唯一“依赖”的事物类是工厂实现的特定接口。您似乎对“紧耦合”实际上是什么有一些误解。也许你应该再看一遍那个视频......
【解决方案4】:

我选择了lazy-injection(即注入一个代理类):

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

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

这里没有直接注入依赖(class2):注入了代理类。只有在使用代理类时才会加载依赖。

这在 PHP-DI(依赖注入框架)中是可能的。

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

【讨论】:

    猜你喜欢
    • 2018-06-10
    • 2020-01-19
    • 1970-01-01
    • 1970-01-01
    • 2016-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多