2012-03-29 24 views
18

Gần đây tôi đã bắt đầu đọc về tiêm phụ thuộc và nó đã làm cho tôi suy nghĩ lại một số thiết kế của tôi.PHP Tải các đối tượng lười biếng và tiêm phụ thuộc

Sự cố tôi gặp phải là: Giả sử tôi có hai lớp: Xe hơi và Hành khách; Đối với những hai lớp tôi có một số người vẽ bản đồ dữ liệu để làm việc với cơ sở dữ liệu: CarDataMapper và PassengerDataMapper

Tôi muốn để có thể làm điều gì đó như thế này trong mã:

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

Trước khi tôi biết bất cứ điều gì về DI, tôi sẽ xây dựng các lớp học của tôi như thế này:

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; 
    } 

} 

tôi cũng sẽ có mã tương tự như trong phương pháp Passenger-> getCar() để lấy xe chở khách là trong

.

Bây giờ tôi hiểu rằng điều này tạo ra các phụ thuộc (tốt, tôi đã hiểu nó trước đây, nhưng tôi không biết rằng điều này là "sai") giữa các đối tượng Xe và Hành khách và các đối tượng bản đồ dữ liệu.

Trong khi cố gắng để nghĩ ra giải pháp cho hai lựa chọn này xuất hiện trong đầu, nhưng tôi không thực sự thích ai trong số họ:

1: Làm một cái gì đó như thế này:

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

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

Nhưng những gì nếu hành khách có đồ vật mà họ cần tiêm, và điều gì sẽ xảy ra nếu việc lồng nhau lên đến mười hoặc hai mươi cấp ... Tôi sẽ khởi động gần như mọi đối tượng trong phần đầu của ứng dụng, điều này sẽ lần lượt truy vấn toàn bộ cơ sở dữ liệu trong suốt quá trình. Nếu tôi phải gửi hành khách đến một đối tượng khác mà phải làm điều gì đó với các đối tượng mà hành khách nắm giữ, tôi cũng không muốn ngay lập tức khởi tạo các vật thể này.

2: Chích người vẽ bản đồ dữ liệu vào xe và hành khách đối tượng và có một cái gì đó như thế này:

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; 
    } 
} 

Tôi không giống như bất kỳ này tốt hơn, bởi vì nó không giống như các xe thực sự không biết gì về các mapper dữ liệu, và không có người lập bản đồ dữ liệu, Ô tô có thể hoạt động không thể đoán trước (không phải trả lại hành khách, khi thực sự có chúng)

Vì vậy, câu hỏi đầu tiên của tôi là: Tôi có một cách tiếp cận hoàn toàn sai ở đây, bởi vì, tôi càng nhìn vào nó , có vẻ như tôi đang xây dựng một ORM, thay vì một lớp kinh doanh?

Câu hỏi thứ hai là: có cách nào thực sự tách các đối tượng và trình ánh xạ dữ liệu theo cách cho phép tôi sử dụng các đối tượng như được mô tả trong khối mã đầu tiên không?

Thứ ba câu hỏi: Tôi đã nhìn thấy một số câu trả lời cho các ngôn ngữ khác (một số phiên bản của C, tôi nghĩ) giải quyết vấn đề này với một cái gì đó như thế này được mô tả ở đây: What is the proper way to inject a data access dependency for lazy loading? Như tôi đã không có thời gian để chơi với khác ngôn ngữ, điều này không có ý nghĩa với tôi, vì vậy tôi sẽ biết ơn nếu ai đó giải thích các ví dụ trong liên kết trong PHP-ish.Tôi cũng đã xem xét một số khuôn khổ DI và đọc về DI Containers và Inversion of Control, nhưng từ những gì tôi hiểu chúng được sử dụng để xác định và tiêm phụ thuộc cho các lớp 'không động', ví dụ, Ô tô sẽ phụ thuộc vào Engine, nhưng nó sẽ không cần động cơ được nạp động từ db, nó sẽ đơn giản được khởi tạo và tiêm vào trong xe.

Xin lỗi vì bài đăng dài và cảm ơn trước.

+2

Bạn có thể muốn thanh toán [PoEAA] (http://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420/ref=sr_1_1?ie=UTF8&qid=1350785602&sr=8-1&keywords= mẫu + của + doanh nghiệp + ứng dụng + kiến ​​trúc) Mặc dù nó sẽ không giúp ích gì cả với [DI] (http://martinfowler.com/articles/injection.html) (nó có trước sự phổ biến), bạn sẽ nhận ra rằng ngay cả với Business Layer bạn sẽ cần một ORM nằm dưới nó. Bạn nên sử dụng [Data Mapper] (http://martinfowler.com/eaaCatalog/dataMapper.html). 1 để đặt câu hỏi tôi đã tìm kiếm câu trả lời. – xenoterracide

+0

Cảm ơn mẹo.Gần đây tôi đã sử dụng Doctrine2 trong hầu hết các dự án của tôi, đó là một ORM Mapper dữ liệu và trở nên khá dễ làm việc. – Pinetree

Trả lời

5

tôi sẽ nói "Tôi biết đây là một câu hỏi cũ nhưng ... "sau đó tôi nhận ra bạn đã đăng nó cách đây 9 giờ, điều đó thật tuyệt vời, bởi vì tôi vừa mới đạt được 'độ phân giải' thỏa đáng cho bản thân mình. Tôi nghĩ về việc thực hiện và sau đó tôi nhận ra đó là những gì mọi người gọi là 'tiêm phụ thuộc'.

Dưới đây là một ví dụ:

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); 
    } 

} 

Sau đó, trong lớp dịch vụ của bạn (nơi bạn phối hợp 'hành động giữa nhiều người vẽ bản đồ và các mô hình), bạn có thể gọi phương thức 'setReplyFetcher' trên tất cả các đối tượng vé trước bạn trả lại chúng cho bất kỳ cái gì gọi lớp dịch vụ - HOẶC - bạn có thể làm điều gì đó rất giống với mỗi người lập bản đồ, bằng cách cung cấp cho người lập bản đồ một tài sản 'fetcherInstance' và 'callback' riêng cho mỗi người lập bản đồ đối tượng sẽ cần, và sau đó thiết lập R inNG trong lớp dịch vụ, khi đó người lập bản đồ sẽ xử lý các đối tượng. Tôi vẫn đang cân nhắc sự khác biệt giữa hai cách tiếp cận.

Ví dụ về phối hợp trong lớp dịch vụ:

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; 
    } 
} 

Dù bằng cách nào bạn đi, các lớp đối tượng độc lập với lớp mapper, và các lớp mapper là độc lập với nhau.

EDIT: Dưới đây là một ví dụ về các cách khác để làm điều đó:

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

Như bạn có thể thấy, các mapper đòi hỏi các nguồn lực khác để được cung cấp cho thậm chí được khởi tạo. Như bạn có thể thấy, với phương pháp này, bạn có thể hạn chế gọi các hàm bản đồ với các id đối tượng làm tham số. Tôi chắc chắn với một số ngồi xuống và nghĩ rằng có một giải pháp thanh lịch để kết hợp các thông số khác, nói lấy vé 'mở' so với vé 'đóng' thuộc về một đối tượng bộ phận.

+0

Nếu tôi hiểu câu trả lời của bạn một cách chính xác, điều này giống với những gì tôi có trong giải pháp số 2 (đưa những người lập bản đồ dữ liệu vào các đối tượng), với sự khác biệt là tôi gọi phương thức mapper trực tiếp. phải "biết" phương thức nào để gọi, ngay cả khi người lập bản đồ được tiêm), và bạn gọi nó bằng cách sử dụng một giá trị chuỗi được tiêm như một cuộc gọi lại, điều này sẽ mang lại sự linh hoạt bổ sung. Tôi chỉ cần kiểm tra kỹ ở đây nếu tôi hiểu đúng. – Pinetree

+0

Điều đó là chính xác. Bằng cách này, đối tượng không cần phải được mã hóa cứng với bất kỳ lớp/cá thể của trình ánh xạ cụ thể hoặc gọi lại. – tuespetre

+0

Cảm ơn câu trả lời ở đây, họ chắc chắn đã cho tôi suy nghĩ trong những gì bây giờ có vẻ đúng hướng. Tôi đã thực hiện nó hơi khác nhau, mặc dù, bằng cách sử dụng những người lập bản đồ phải tuân theo một giao diện được quy định và các đối tượng proxy mà tôi chèn các trình lập bản đồ cho các thực thể liên quan. – Pinetree

10

Có thể tắt chủ đề, nhưng tôi nghĩ rằng nó sẽ giúp bạn một chút:

Tôi nghĩ rằng bạn cố gắng đạt được giải pháp hoàn hảo. Nhưng không có vấn đề gì bạn đưa ra, trong một vài năm, bạn sẽ có nhiều kinh nghiệm và bạn chắc chắn sẽ có thể cải thiện thiết kế của bạn. Trong những năm qua với các đồng nghiệp của chúng tôi, chúng tôi đã phát triển nhiều ORM/Mô hình kinh doanh, nhưng đối với hầu hết mọi dự án mới, chúng tôi bắt đầu từ đầu, vì mọi người đều có kinh nghiệm hơn, mọi người đã học được từ những sai lầm trước đó và mọi người đã gặp phải với các mẫu và ý tưởng mới. Tất cả những gì đã thêm một tháng hoặc lâu hơn trong phát triển, làm tăng chi phí của sản phẩm cuối cùng.

Bất kể công cụ tốt đến mức nào, vấn đề chính là sản phẩm cuối cùng phải càng tốt càng tốt, với chi phí tối thiểu. Khách hàng sẽ không quan tâm và sẽ không trả tiền cho những thứ không thể nhìn thấy hoặc hiểu được.

Trừ khi, tất nhiên, bạn viết mã cho nghiên cứu hoặc để giải trí.

TL; DR: Bản thân tương lai của bạn sẽ luôn vượt trội hơn bản thân hiện tại của bạn, vì vậy đừng nên nghĩ đến nó. Chỉ cần chọn một cách cẩn thận một giải pháp làm việc, làm chủ nó và gắn bó với nó cho đến khi nó sẽ không giải quyết vấn đề của bạn: D

Để trả lời câu hỏi của bạn:

  1. Mã của bạn là hoàn toàn tốt đẹp, nhưng bạn càng sẽ cố gắng làm cho nó "thông minh" hoặc "trừu tượng" hoặc "không phụ thuộc", bạn càng nghiêng về phía ORM.

  2. Điều bạn muốn trong khối mã đầu tiên là khá khả thi. Hãy nhìn vào cách thuyết ORM công trình, hoặc phương pháp ORM rất đơn giản này tôi đã làm cách đây vài tháng cho một dự án cuối tuần:

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

+0

Tôi hiểu hoàn toàn ý bạn bằng cách cung cấp cho khách hàng sản phẩm tốt nhất có thể với chi phí tối thiểu. Nhưng vì đây là một kỹ thuật tôi có thể sử dụng trong nhiều dự án, và tôi đang cố gắng sử dụng nó trong một số dự án của riêng mình mà không bị ràng buộc bởi thời gian hay tiền bạc, tôi muốn nắm bắt tốt nó, mặc dù tôi có thể sẽ làm điều gì đó hoàn toàn khác trong vài tháng/năm. – Pinetree

+0

Một ORM sẽ cung cấp cho bạn một nỗi đau lớn một nỗi đau trong một ứng dụng hơi phức tạp hơn một bộ sưu tập đĩa CD. Rất nhiều khuyết điểm. – user151851

+1

Hai năm sau, tôi ước tôi có thể upvote điều này một lần nữa và loại bỏ tất cả các upvotes từ câu trả lời của tôi. : p – tuespetre

1

Đây là một cách tiếp cận khác mà tôi đã nghĩ đến. Bạn có thể tạo một đối tượng 'DAOInjection' hoạt động như một vùng chứa cho DAO cụ thể, gọi lại, và args cần thiết để trả về các đối tượng mong muốn. Các lớp sau đó chỉ cần biết về lớp DAOInjection này, vì vậy chúng vẫn được tách khỏi tất cả các DAO/mappers/services/etc của bạn.

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; 
    } 
} 

Bạn cũng có thể chuyển đối số 'InjectionHelper' làm đối số. Các InjectionHelper hoạt động như một container gọi lại - theo cách này, nếu bạn cần phải vượt qua bất kỳ thông tin về các đối tượng lười biếng tải DAO tiêm của nó, bạn sẽ không phải cứng mã nó vào đối tượng. Thêm vào đó, nếu bạn cần các phương pháp 'ống' cùng nhau - giả sử bạn cần phải vượt qua $this->getDepartment()->getManager()->getId() đến DAO được tiêm cho bất kỳ lý do gì - bạn có thể. Đơn giản chỉ cần vượt qua nó như getDepartment|getManager|getId đến constructor của 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; 
    } 
} 

Để thực hiện chức năng này trong đối tượng, bạn cần phải tiêm vào lúc xây dựng để đảm bảo rằng đối tượng có hoặc có thể nhận được tất cả thông tin cần thiết. Mỗi phương thức sử dụng phép tiêm đơn giản gọi phương thức execute() của DAOInjection tương ứng.

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; 
    } 
} 

tôi sẽ sau đó, trong constructor của lớp dịch vụ của tôi, nhanh chóng vẽ bản đồ có liên quan đến dịch vụ mà sử dụng các lớp DAOInjection có liên quan như các đối số cho nhà xây dựng của người vẽ bản đồ. Những người lập bản đồ sẽ chăm sóc đảm bảo mỗi đối tượng có các mũi tiêm của nó, bởi vì công việc của người lập bản đồ là trả về các đối tượng hoàn chỉnh và xử lý việc lưu/xóa các đối tượng, trong khi công việc của dịch vụ là điều phối các mối quan hệ giữa những người lập bản đồ, đối tượng khác nhau. trên. Cuối cùng, bạn có thể sử dụng nó để tiêm callbacks cho các dịch vụ HOẶC người lập bản đồ, vì vậy hãy nói rằng bạn muốn đối tượng 'Ticket' của bạn truy xuất một người dùng cha mẹ, xảy ra bên ngoài lĩnh vực của 'Ticket Service' - dịch vụ bán vé chỉ có thể tiêm một cuộc gọi lại đến 'Dịch vụ người dùng', và nó sẽ không phải biết một điều về cách DAL hoạt động cho các đối tượng khác.

Hy vọng điều này sẽ hữu ích!

Các vấn đề liên quan