我最近开始阅读有关依赖注入的内容,这让我重新思考了我的一些设计。
我遇到的问题是这样的: 比方说我有两个班:汽车和乘客; 对于这两个类,我有一些数据映射器来处理数据库: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容器和控制反转的内容,但据我所知,它们用于定义和注入“非动态”类的依赖关系,例如,汽车将依赖在引擎上,但它不需要从数据库动态加载引擎,它只是被实例化并注入汽车。
对于冗长的帖子表示歉心,并提前致谢。
答案 0 :(得分:10)
也许偏离主题,但我认为它会对你有所帮助:
我认为您尝试实现完美的解决方案。但无论你想出什么,在几年内,你将会更有经验,你一定能够改进你的设计。
在过去的几年里,我和同事一起开发了许多ORM /商业模式,但几乎每个新项目我们都是从头开始,因为每个人都经验丰富,每个人都从以前的错误中吸取教训,每个人都遇到过新的模式和想法。所有这些都增加了一个月左右的开发时间,这增加了最终产品的成本。
无论工具有多好,关键问题是最终产品必须以最低成本尽可能好。客户不会关心也不会为无法看到或理解的东西付费。
当然,除非您为研究或娱乐编码。
TL; DR:你未来的自我将总是超越你现在的自我,所以不要过分思考它。只需仔细挑选一个有效的解决方案,掌握它并坚持下去,直到它无法解决您的问题:D
回答你的问题:
你的代码完全没问题,但你会越努力使它“聪明”或“抽象”或“无依赖”,你就越倾向于ORM。
你想要的第一个代码块是非常可行的。看一下Doctrine ORM是如何工作的,或者是几个月前我为周末项目做的非常简单的ORM方法:
答案 1 :(得分: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)
}
}
}
如您所见,映射器要求其他资源可用于甚至实例化。您还可以看到,使用此方法,您只能使用对象ID作为参数调用映射器函数。我确信有些人会坐下来认为有一个优雅的解决方案可以包含其他参数,例如获取“开放”门票与属于部门对象的“关闭”门票。
答案 2 :(得分:1)
这是我想到的另一种方法。您可以创建一个“DAOInjection”对象,该对象充当返回所需对象所需的特定DAO,回调和args的容器。这些类只需要了解这个DAOInjection类,因此它们仍然与所有DAO / mappers / services /等分离。
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如何为其他对象工作。
希望这有帮助!