2012-03-29 292 views
18

我最近开始阅读关于依赖注入的知识,它让我重新思考一些我的设计。PHP懒惰加载对象和依赖项注入

我的问题是这样的: 比方说,我有两个类:车客; 对于这两个类,我有一些数据映射器与数据库的工作: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; 
    } 
} 

我不喜欢这样更好,因为它不喜欢的车是真的不知道数据映射器,并没有数据映射器,汽车可能表现不可预知(不返回乘客,当它实际上有他们)

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

第二个问题是: 是否有一种方法可以实际将对象和数据映射器分离开来,这样就可以使用第一个代码块中描述的对象?

第三个问题: 我已经看到了其他语言解决这个问题的是这样一些答案(有些版本的C,我认为)这里描述: What is the proper way to inject a data access dependency for lazy loading? 由于我没有来得及与其他玩语言,这对我来说没有意义,所以如果有人会在PHP-ish的链接中解释这些例子,我将不胜感激。

我也查看了一些DI框架,并阅读了关于DI Containers和Inversion of Control的内容,但是根据我的理解,它们用于定义和注入“非动态”类的依赖关系,例如Car依赖于引擎,但它不需要从数据库动态加载引擎,它只会被实例化并注入汽车。

对不起,冗长的职位,并提前感谢。

+2

您可能想要结帐[PoEAA](http://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420/ref=sr_1_1?ie=UTF8&qid=1350785602&sr=8-1&keywords=模式+ +企业+应用程序+架构)虽然它根本无法帮助[DI](http://martinfowler.com/articles/injection.html)(它早于共同点),但您会意识到,即使您业务层您需要一个位于其下的ORM。建议使用[Data Mapper](http://martinfowler.com/eaaCatalog/dataMapper.html)。 +1提出我一直在寻找答案的问题。 – xenoterracide 2012-10-21 02:16:06

+0

感谢您的提示。最近我在我的大部分项目中都使用了Doctrine2,这是一个Data Mapper ORM,并且很容易处理。 – Pinetree 2012-10-21 06:02:28

回答

5

我会说:“我知道这是一个但是......“然后我意识到你在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'属性给对象需要的每个映射器,然后在服务层中设置THAT,然后映射器将负责准备对象。我仍然在权衡这两种方法之间的差异。

在服务层协调的示例:

你去
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) 
     } 
    } 
} 

正如你所看到的,映射需要的其他资源可用,甚至被实例化。正如你所看到的,使用这种方法,你只能用object id作为参数来调用mapper函数。我确信有些人会坐下来,并认为有一个优雅的解决方案可以包含其他参数,比如说取得属于某个部门对象的'开放'门票与'封闭'门票。

+0

如果我正确地理解了你的回复,这与我在解决方案2(将数据映射器注入到对象中)中的相似,区别在于我直接调用映射器方法(创建依赖项必须“知道”调用哪个方法,即使映射器被注入),并且使用注入的字符串值作为回调来调用它,这会提供一些额外的灵活性。如果我理解正确,我只是在这里仔细检查。 – Pinetree 2012-03-30 06:41:31

+0

这是正确的。这样,对象不必使用任何特定的映射器类/实例或回调进行硬编码。 – tuespetre 2012-03-30 15:42:53

+0

感谢这里的答案,他们当然让我思考,现在看来是正确的方向。不过,我用稍微不同的映射器来实现,这些映射器必须符合规定的接口和代理对象,其中我为相关实体注入映射器。 – Pinetree 2012-04-13 22:34:29

10

可能偏离主题,但我认为,这将有助于你一点:

我认为你试图达到了完美的解决方案。但无论你想出什么,几年后,你会更有经验,你一定能够改进你的设计。

在过去的几年中,我们与同事一起开发了许多ORM /业务模型,但几乎每个新项目都是从头开始的,因为每个人都更有经验,每个人都从前面的错误中学到了东西,新的模式和想法。所有这些都增加了一个月左右的开发时间,这增加了最终产品的成本。

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

当然,除非你为研究或趣味编码。

TL; DR:你未来的自我总是会胜过你目前的自我,所以不要过度地去思考它。只要仔细挑选工作解决方案,掌握它,并坚持下去,直到它不会解决你的问题:d

回答您的问题:

  1. 你的代码是完全正常的,但你越会尝试使其变得“聪明”或“抽象”或“无依赖性”,您就越倾向于使用ORM。

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

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

+0

我完全理解您以最低的成本为客户提供最佳产品的可能性。但是,由于这是一种我可能在多个项目中使用的技术,因此我试图在一些不受时间或金钱约束的自己的项目中使用它,我希望能够很好地掌握尽管我可能会在几个月/几年内完全不同。 – Pinetree 2012-03-30 06:46:15

+0

在一个比CD收​​藏引物更复杂的应用程序中,ORM会带给你极大的痛苦和悲伤。很多缺点。 – user151851 2013-01-25 20:16:52

+1

两年后,我希望我可以再次提出这一点,并从我的答案中删除所有upvotes。 :p – tuespetre 2014-05-21 19:22:33

1

这是我想到的另一种方法。您可以创建一个'DAOInjection'对象,该对象充当特定DAO,回调以及返回所需对象所需的参数的容器。这些类只需要知道这个DAOInjection类,所以它们仍然与所有的DAO/mappers/services/etc分离。

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->getDepartment()->getManager()->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类作为映射器建设者论点服务映射器。然后映射器会负责确保每个对象都有其注入,因为映射器的工作是返回完整的对象并处理对象的保存/删除,而服务的工作是协调各种映射器,对象等之间的关系上。

最终,您可以使用它来为服务或映射器注入回调,所以假设您希望您的'Ticket'对象检索父级用户,这恰好不在'Ticket Service'领域 - 票务服务可以将回调注入到“用户服务”中,而不必知道DAL如何为其他对象工作。

希望这会有所帮助!