2012-03-29 35 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如何爲其他對象工作。

希望這會有所幫助!