PHP延迟加载对象和依赖注入

Posted

技术标签:

【中文标题】PHP延迟加载对象和依赖注入【英文标题】:PHP Lazy loading objects and dependency injection 【发布时间】:2012-04-12 23:03:45 【问题描述】:

我最近开始阅读有关依赖注入的内容,这让我重新思考了我的一些设计。

我遇到的问题是这样的: 假设我有两个班级:汽车和乘客; 对于这两个类,我有一些数据映射器可以与数据库一起使用:CarDataMapper 和PassengerDataMapper

我希望能够在代码中做这样的事情:

$car = CarDataMapper->getCarById(23); // returns the car object
foreach($car->getPassengers() as $passenger) // returns all passengers of that car
    $passenger->doSomething();

在我了解 DI 之前,我会这样构建我的课程:

class Car 
    private $_id;
    private $_passengers = null;

    public function getPassengers()
        if($this->_passengers === null)
            $passengerDataMapper = new PassengerDataMapper;
            $passengers = $passengerDataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        
        return $this->_passengers;  
    


Passenger->getCar() 方法中也有类似的代码来获取乘客所在的汽车。

我现在明白这会在 Car 和 Passenger 对象以及数据映射器对象之间产生依赖关系(好吧,我之前也理解过,但我不知道这是“错误的”)。

在尝试考虑这两个选项的解决方案时,我想到了,但我真的不喜欢其中任何一个:

1:做这样的事情:

$car = $carDataMapper->getCarById(23);
$passengers = $passengerDataMapper->getPassengersByCarId($car->getId());
$car->setPassengers($passengers);

foreach($car->getPassengers() as $passenger)
    $passenger->doSomething();

但是如果乘客有他们需要注入的对象,如果嵌套达到十或二十层怎么办......我会在我的应用程序开始时实例化几乎每个对象,这反过来又会查询整个过程中的数据库。 如果我必须将乘客发送到另一个必须对乘客持有的对象做某事的对象,我也不想立即实例化这些对象。

2:将数据映射器注入到汽车和乘客对象中,并具有以下内容:

class Car 
    private $_id;
    private $_passengers = null;
    private $_dataMapper = null;

    public function __construct($dataMapper)
        $this->setDataMapper($dataMapper);
    

    public function getPassengers()
        if($this->_passengers === null && $this->_dataMapper instanceof PassengerDataMapper)
            $passengers = $this->_dataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        
        return $this->_passengers;  
    

我不喜欢这个更好,因为它并不是说 Car 真的不知道数据映射器,如果没有数据映射器,Car 可能会出现不可预测的行为(不是返回的乘客,但实际上有他们)

所以我的第一个问题是: 我是不是在这里采取了完全错误的方法,因为我越看它,就越像是在构建 ORM,而不是业务层?

第二个问题是: 有没有办法以一种允许我使用第一个代码块中描述的对象的方式实际解耦对象和数据映射器?

第三个问题: 我已经看到了其他语言的一些答案(我认为是 C 的某些版本)解决了这个问题,如下所述: What is the proper way to inject a data access dependency for lazy loading? 由于我没有时间玩其他语言,这对我来说毫无意义,所以如果有人能用 php-ish 解释链接中的示例,我将不胜感激。

我还查看了一些 DI 框架,并阅读了有关 DI 容器和控制反转的信息,但据我了解,它们用于定义和注入“非动态”类的依赖项,例如,汽车将依赖在 Engine 上,但它不需要从 db 动态加载引擎,它只需实例化并注入 Car。

抱歉,帖子冗长,提前致谢。

【问题讨论】:

你可能想签出PoEAA 虽然它对DI 没有任何帮助(它早于通用性)你会意识到即使你的业务层你也需要一个 ORM坐在它下面。推荐Data Mapper。 +1 提出我一直在寻找答案的问题。 感谢您的提示。最近我在我的大部分项目中都使用了 Doctrine2,它是一个 Data Mapper ORM,并且非常容易使用。 【参考方案1】:

也许题外话,但我认为它会帮助你一点:

我认为您试图实现完美的解决方案。但不管你想出什么,几年后,你会更有经验,你一定能改进你的设计。

在过去的几年里,我们和我的同事们开发了许多 ORM / 商业模式,但几乎每个新项目我们都是从零开始,因为每个人都更有经验,每个人都从以前的错误中吸取教训,每个人都遇到过新的模式和想法。所有这些都在开发过程中增加了一个月左右的时间,从而增加了最终产品的成本。

无论工具多么好,关键问题是最终产品必须尽可能好,成本最低。客户不会关心,也不会为看不到或无法理解的东西付费。

当然,除非您编写代码是为了研究或娱乐。

TL;DR:未来的自己总是比现在的自己聪明,所以不要想太多。仔细挑选一个可行的解决方案,掌握它并坚持下去,直到它不能解决你的问题:D

回答您的问题:

    您的代码非常好,但您越是尝试使其“聪明”或“抽象”或“无依赖”,您就越倾向于 ORM。

    李>

    你想要的第一个代码块是非常可行的。看看 Doctrine ORM 是如何工作的,或者我几个月前为一个周末项目做的这个非常简单的 ORM 方法:

https://github.com/aletzo/dweet/blob/master/app/models

【讨论】:

我完全理解您所说的以最低成本为客户提供最好的产品的意思。但由于这是一种我可能会在多个项目中使用的技术,并且我正在尝试在我自己的一些不受时间或金钱约束的项目中使用它,我想很好地掌握它,即使我可能会在几个月/几年内做一些完全不同的事情。 ORM 在比 CD 集合入门更复杂的应用程序中会给您带来极大的痛苦。这么多缺点。 两年后,我希望我能再次对此投票,并从我的答案中删除所有投票。 :p【参考方案2】:

我本来想说“我知道这是一个老问题,但是......”然后我意识到你在 9 小时前发布了它,这很酷,因为我刚刚为自己找到了一个令人满意的“解决方案”。我想到了实现,然后我意识到这就是人们所说的“依赖注入”。

这是一个例子:

class Ticket 

    private $__replies;
    private $__replyFetcher;
    private $__replyCallback;
    private $__replyArgs;

    public function setReplyFetcher(&$instance, $callback, array $args) 
        if (!is_object($instance))
            throw new Exception ('blah');
        if (!is_string($callback))
            throw new Exception ('blah');
        if (!is_array($args) || empty($args))
            throw new Exception ('blah');
        $this->__replyFetcher = $instance;
        $this->__replyCallback = $callback;
        $this->__replyArgs = $args;
        return $this;
    

    public function getReplies () 
        if (!is_object($this->__replyFetcher)) throw new Exception ('Fetcher not set');
        return call_user_func_array(array($this->__replyFetcher,$this->__replyCallback),$this->__replyArgs);
    


然后,在您的服务层(您在多个映射器和模型之间“协调”操作)中,您可以在所有票证对象上调用“setReplyFetcher”方法,然后再将它们返回到调用服务层的任何对象——或者-- 你可以对每个映射器做一些非常相似的事情,通过给映射器一个私有的'fetcherInstance'和'callback'属性来为对象将需要的每个映射器,然后在服务层中设置它,然后映射器将注意准备物品。我仍在权衡这两种方法之间的差异。

服务层协调示例:

class Some_Service_Class 
    private $__mapper;
    private $__otherMapper;
    public function __construct() 
        $this->__mapper = new Some_Mapper();
        $this->__otherMapper = new Some_Other_Mapper();
    
    public function getObjects() 
        $objects = $this->__mapper->fetchObjects();
        foreach ($objects as &$object) 
            $object->setDependentObjectFetcher($this->__otherMapper,'fetchDependents',array($object->getId()));
        
        return $objects;
    

不管怎样,对象类是独立于映射器类的,而映射器类是相互独立的。

编辑:这是另一种方法的示例:

class Some_Service 
    private $__mapper;
    private $__otherMapper;
    public function __construct()
        $this->__mapper = new Some_Mapper();
        $this->__otherMapper = new Some_Other_Mapper();
        $this->__mapper->setDependentFetcher($this->__otherMapper,'someCallback');
    
    public function fetchObjects () 
        return $this->__mapper->fetchObjects();
            


class Some_Mapper 
    private $__dependentMapper;
    private $__dependentCallback;
    public function __construct ( $mapper, $callback ) 
        if (!is_object($mapper) || !is_string($callback)) throw new Exception ('message');
        $this->__dependentMapper = $mapper;
        $this->__dependentCallback = $callback;
        return $this;
    
    public function fetchObjects() 
        //Some database logic here, returns $results
        $args[0] = &$this->__dependentMapper;
        $args[1] = &$this->__dependentCallback;
        foreach ($results as $result) 
            // Do your mapping logic here, assigning values to properties of $object
            $args[2] = $object->getId();
            $objects[] = call_user_func_array(array($object,'setDependentFetcher'),$args)
        
    

如您所见,映射器需要其他资源才能被实例化。正如您还可以看到的,使用此方法您仅限于以对象 id 作为参数调用映射器函数。我敢肯定,有些人坐下来认为有一个优雅的解决方案可以合并其他参数,比如获取属于部门对象的“开放”票证与“关闭”票证。

【讨论】:

如果我正确理解了您的回复,这与我在解决方案 2 中的类似(将数据映射器注入对象),不同之处在于我直接调用映射器方法(创建对象必须“知道”调用哪个方法的依赖项,即使映射器被注入),并且您使用注入的字符串值作为回调来调用它,这将提供一些额外的灵活性。如果我理解正确,我只是在这里仔细检查。 没错。这样,对象就不必使用任何特定的映射器类/实例或回调进行硬编码。 感谢您的回答,他们确实让我想到了目前看来正确的方向。不过,我的实现方式略有不同,使用必须符合规定接口的映射器和代理对象,我为相关实体注入映射器。【参考方案3】:

这是我想到的另一种方法。您可以创建一个“DAOInjection”对象,该对象充当特定 DAO、回调和返回所需对象所需的参数的容器。然后这些类只需要知道这个 DAOInjection 类,因此它们仍然与您的所有 DAO/映射器/服务/等解耦。

class DAOInjection 
    private $_DAO;
    private $_callback;
    private $_args;
    public function __construct($DAO, $callback, array $args)
        if (!is_object($DAO)) throw new Exception;
        if (!is_string($callback)) throw new Exception;
        $this->_DAO = $DAO;
        $this->_callback = $callback;
        $this->_args = $args;
    
    public function execute( $objectInstance ) 
        if (!is_object($objectInstance)) throw new Exception;
        $args = $this->_prepareArgs($objectInstance);
        return call_user_func_array(array($this->_DAO,$this->_callback),$args);
    
    private function _prepareArgs($instance) 
        $args = $this->_args;
        for($i=0; $i < count($args); $i++)
            if ($args[$i] instanceof InjectionHelper) 
                $helper = $args[$i];
                $args[$i] = $helper->prepareArg($instance);
            
        
        return $args;
    

您还可以将“InjectionHelper”作为参数传递。 InjectionHelper 充当另一个回调容器——这样,如果您需要将有关延迟加载对象的任何信息传递给其注入的 DAO,您就不必将其硬编码到对象中。另外,如果您需要将方法“管道”在一起——比如说你需要将$this-&gt;getDepartment()-&gt;getManager()-&gt;getId() 传递给注入的DAO,无论出于何种原因——你都可以。只需像 getDepartment|getManager|getId 一样将它传递给 InjectionHelper 的构造函数。

class InjectionHelper 
    private $_callback;
    public function __construct( $callback ) 
        if (!is_string($callback)) throw new Exception;
        $this->_callback = $callback;
    
    public function prepareArg( $instance ) 
        if (!is_object($instance)) throw new Exception;
        $callback = explode("|",$this->_callback);
        $firstCallback = $callback[0];
        $result = $instance->$firstCallback();
        array_shift($callback);
        if (!empty($callback) && is_object($result)) 
            for ($i=0; $i<count($callback); $i++) 
                $result = $result->$callback[$i];
                if (!is_object($result)) break;
            
        
        return $result;
    

要在对象中实现此功能,您需要在构造时进行注入,以确保对象拥有或可以获得所需的所有信息。每个使用注入的方法都简单地调用相应 DAOInjection 的 execute() 方法。

class Some_Object 
    private $_childInjection;
    private $_parentInjection;
    public function __construct(DAOInjection $childInj, DAOInjection $parInj) 
        $this->_childInjection = $childInj;
        $this->_parentInjection = $parInj;
    
    public function getChildObjects() 
        if ($this->_children == null)
            $this->_children = $this->_childInjection->execute($this);
        return $this->_children;
    
    public function getParentObjects() 
        if ($this->_parent == null)
            $this->_parent = $this->_parentInjection->execute($this);
        return $this->_parent;
    

然后,我将在我的服务类的构造函数中,使用相关的 DAOInjection 类作为映射器构造函数的参数来实例化与该服务相关的映射器。然后映射器将负责确保每个对象都有其注入,因为映射器的工作是返回完整的对象并处理对象的保存/删除,而服务的工作是协调各种映射器、对象等之间的关系开。

最终,您可以使用它来向服务或映射器注入回调,因此假设您希望您的“票证”对象检索父用户,这恰好在“票证服务”范围之外——票证服务可以只需向“用户服务”注入一个回调,它就不必知道 DAL 如何为其他对象工作。

希望这会有所帮助!

【讨论】:

以上是关于PHP延迟加载对象和依赖注入的主要内容,如果未能解决你的问题,请参考以下文章

HIbernate学习笔记4 之 延迟加载和 关联映射

关于Spring IOC (DI-依赖注入),你需要知道的 案例版

MyBatis延迟加载和缓存

MyBatis延迟加载和缓存

Spring IOC源码剖析:lazy-init 延迟加载机制和循环依赖问题

hibernate 延迟加载和抓取策略