[英]PHP Lazy loading objects and dependency injection
我最近開始閱讀有關依賴注入的內容,它讓我重新思考了我的一些設計。
我遇到的問題是:讓我說我有兩個班:汽車和乘客; 對於這兩個類,我有一些數據映射器來處理數據庫: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)的一些答案,解決了這個問題,如下所述: 為延遲加載注入數據訪問依賴的正確方法是什么? 由於我沒有時間玩其他語言,這對我來說沒有任何意義,所以如果有人在PHP-ish中解釋鏈接中的示例,我將不勝感激。
我還查看了一些DI框架,並閱讀了有關DI容器和控制反轉的內容,但據我所知,它們用於定義和注入“非動態”類的依賴關系,例如,汽車將依賴於引擎,但它不需要從db動態加載引擎,它只是被實例化並注入到Car中。
很抱歉這篇冗長的帖子,並提前致謝。
也許偏離主題,但我認為它會對你有所幫助:
我認為你試圖實現完美的解決方案。 但無論你想出什么,在幾年內,你將會更有經驗,你一定能夠改進你的設計。
在過去的幾年里,我和同事一起開發了許多ORM /商業模式,但幾乎每個新項目我們都是從頭開始,因為每個人都經驗豐富,每個人都從以前的錯誤中吸取教訓,每個人都遇到了新的模式和想法。 所有這些都增加了一個月左右的開發時間,這增加了最終產品的成本。
無論工具有多好,關鍵問題是最終產品必須以最低成本盡可能好。 客戶不會關心也不會為無法看到或理解的東西付費。
當然,除非你為研究或樂趣編碼。
TL; DR:你未來的自我將總是超越你現在的自我,所以不要過分思考它。 只需仔細挑選一個有效的解決方案,掌握它並堅持下去,直到它無法解決您的問題:D
回答你的問題:
你的代碼非常好,但你會越努力使它“聰明”或“抽象”或“無依賴”,你就越傾向於ORM。
你想要的第一個代碼塊是非常可行的。 看看Doctrine ORM的工作原理,或者幾個月前我為周末項目做的非常簡單的ORM方法:
我打算說“我知道這是一個古老的問題,但是......”然后我意識到你在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作為參數調用映射器函數。 我確信有些人會坐下來認為有一個優雅的解決方案可以包含其他參數,例如獲取屬於某個部門對象的“開放”門票和“關閉”門票。
這是我想到的另一種方法。 您可以創建一個“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如何為其他對象工作。
希望這可以幫助!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.