2012-03-29 26 views
18

Niedawno zacząłem czytać o wstrzyknięciu zależności i zmusiło mnie to do przemyślenia niektórych z moich projektów.PHP Leniwe obiekty ładujące i wtrysk zależności

Problem jaki mam jest taki: Załóżmy, że mam dwie klasy: samochód i pasażer; Dla tych dwóch klas Mam kilka mappers danych do pracy z bazą danych: CarDataMapper i PassengerDataMapper

Chcę być w stanie zrobić coś takiego w kodzie:

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

Przed Wiedziałem nic o DI, Chciałbym zbudować moje zajęcia tak:

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

} 

Chciałbym również mieć podobny kod w metodzie przewozu pasażerów> getCar(), aby sprowadzić samochód pasażer jest w

.

Teraz rozumiem, że to tworzy zależności (dobrze, ja też to rozumiałem wcześniej, ale nie wiedziałem, że to jest "złe") między pojazdem a obiektami pasażera i obiektami odwzorowującymi dane.

Starając się wymyślić rozwiązanie tego dwie opcje przyszło mi do głowy, ale ja nie lubię żadnego z nich:

1: Robi coś takiego:

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

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

ale co jeśli pasażerowie mają przedmioty, które muszą wstrzyknąć, a co jeśli gniazdowanie przejdzie na dziesięć lub dwadzieścia poziomów ... Skończyłbym tworzyć niemal każdy obiekt na początku mojej aplikacji, co z kolei wywoływałoby zapytanie do całej bazy danych podczas procesu. Jeśli muszę wysłać pasażera do innego obiektu, który musi coś zrobić z przedmiotami, które zatrzymuje pasażer, nie chcę natychmiast tworzyć tych obiektów.

2: Wstrzyknięcie mappers danych do obiektów samochodów i pasażerów i mających coś takiego:

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

I dont like to lepiej, bo to nie jest jak samochód jest naprawdę nieświadomi odwzorowującego danych, i bez modułu odwzorowującego dane, samochód mógłby zachowywać się nieprzewidywalnie (nie powracający pasażerowie, gdy faktycznie je ma)

Moje pierwsze pytanie brzmi: Podejmuję tutaj całkowicie niewłaściwe podejście, ponieważ im bardziej na to patrzę , tym bardziej wygląda na to, że buduję ORM zamiast warstwy biznesowej?

Drugie pytanie brzmi: czy istnieje sposób oddzielenia obiektów i maperów danych w sposób, który umożliwiłby mi użycie obiektów opisanych w pierwszym bloku kodu?

Trzecie pytanie: Widziałem kilka odpowiedzi dla innych języków (niektóre wersje C, jak sądzę) rozwiązaniu tego problemu z czymś takim opisane tutaj: What is the proper way to inject a data access dependency for lazy loading? Ponieważ nie miałem czasu, aby bawić się z innymi Języki, to nie ma dla mnie sensu, więc byłbym wdzięczny, gdyby ktoś wyjaśnił przykłady w odnośniku w PHP-ish.

Przyjrzałem się także niektórym strukturom DI i przeczytałem o kontenerach DI i Inwersji kontroli, ale z tego, co zrozumiałem, są one używane do definiowania i wstrzykiwania zależności dla "nie-dynamicznych" klas, gdzie na przykład samochód Zależy od silnika, ale nie wymaga dynamicznego ładowania silnika z bazy danych, po prostu byłby instancjonowany i wstrzykiwany do samochodu.

Przepraszamy za długi post i dziękujemy z góry.

+2

Być może zechcesz wymeldować się [PoEAA] (http://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420/ref=sr_1_1?ie=UTF8&qid=1350785602&sr=8-1&keywords= schematy + + enterprise + application + architecture) Mimo, że nie pomoże to w ogóle z [DI] (http://martinfowler.com/articles/injection.html) (to poprzedza powszechność) zdasz sobie sprawę, że nawet z Warstw biznesowych potrzebujesz ORM, która znajduje się pod nim. Zaleca się [Data Mapper] (http://martinfowler.com/eaaCatalog/dataMapper.html). +1 za pytanie, na które szukałem odpowiedzi. – xenoterracide

+0

Dzięki za cynk.Ostatnio używam Doctrine2 w większości moich projektów, która jest ORM Data Mapper i okazuje się być całkiem łatwa w użyciu. – Pinetree

Odpowiedz

5

miałem zamiar powiedzieć „wiem, że to jest stare pytanie, ale ... "wtedy zdałem sobie sprawę, że napisałeś to 9 godzin temu, co jest fajne, ponieważ właśnie osiągnąłem satysfakcjonującą" rozdzielczość "dla siebie. Myślałem o implementacji, a potem zdałem sobie sprawę, że ludzie nazywają to "zastrzykiem zależności".

Oto przykład:

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

} 

Następnie w warstwie usług (gdzie „koordynować” działań pomiędzy wieloma twórcami map i modeli) można nazwać „setReplyFetcher metody” na wszystkich obiektów biletowych przed zwracasz je do tego, co wywołuje warstwę usługi - LUB - możesz zrobić coś bardzo podobnego z każdym programem odwzorowującym, poprzez nadanie programowi odwzorowania prywatnej właściwości "fetcherInstance" i "wywołania zwrotnego" dla każdego programu odwzorowującego, którego obiekt będzie potrzebował, oraz następnie ustaw THAT w warstwie serwisowej, wówczas program odwzorowujący zajmie się przygotowaniem obiektów. Nadal rozważam różnice między tymi dwoma podejściami.

Przykład koordynację w warstwie usług:

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

czy inaczej pójdziesz, klas obiektów są niezależne od klas Mapper, a zajęcia Mapper są niezależne od siebie.

EDIT: Tutaj jest przykład inny sposób to zrobić:

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

Jak widać, element odwzorowujący wymaga innych środków, aby mieć dostęp do nawet być instancja. Jak widać, ta metoda ogranicza się do wywoływania funkcji odwzorowania z identyfikatorami obiektów jako parametrami. Jestem pewien, że niektórzy siadają i myślą, że istnieje eleganckie rozwiązanie, w którym można uwzględnić inne parametry, np. Pobieranie "otwartych" biletów w porównaniu z "zamkniętymi" biletami należącymi do obiektu departamentu.

+0

Jeśli dobrze zrozumiałem twoją odpowiedź, jest to podobne do tego, co mam w rozwiązaniu numer 2 (wstrzykiwanie maperów danych do obiektów), z tą różnicą, że wywołuję metodę odwzorowania bezpośrednio (tworząc zależność, w której obiekt musi "wiedzieć", którą metodę wywołać, nawet jeśli wstrzykiwacz jest wstrzykiwany), a ty wywołujesz ją za pomocą wstrzykniętej wartości ciągu jako wywołania zwrotnego, co dałoby dodatkową elastyczność. Sprawdzam tylko dwa razy, jeśli dobrze zrozumiałem. – Pinetree

+0

To prawda. W ten sposób obiekt nie musi być zakodowany na stałe za pomocą konkretnej klasy/instancji programu odwzorowującego lub wywołania zwrotnego. – tuespetre

+0

Dzięki za odpowiedzi tutaj, z pewnością sprawili, że zastanawiałem się nad tym, co wydaje się teraz właściwym kierunkiem. Zaimplementowałem go jednak nieco inaczej, używając mapperów, które muszą być zgodne z zalecanym interfejsem i obiektami proxy, do których wstrzykuję mappers dla powiązanych podmiotów. – Pinetree

10

Może nie na temat, ale myślę, że będzie ci trochę pomóc:

myślę, że starają się osiągnąć idealne rozwiązanie. Ale bez względu na to, co wymyślisz, za kilka lat będziesz bardziej doświadczony i na pewno będziesz w stanie poprawić swój projekt.

W ciągu ostatnich lat wraz z moimi kolegami opracowaliśmy wiele ORM-ów i modeli biznesowych, ale dla prawie każdego nowego projektu zaczynaliśmy od zera, ponieważ wszyscy byli bardziej doświadczeni, wszyscy nauczyli się z poprzednich błędów i wszyscy się spotkali z nowymi wzorami i pomysłami. Wszystko to przyniosło dodatkowy miesiąc na rozwój, co zwiększyło koszty produktu końcowego.

Niezależnie od tego, jak dobre są narzędzia, kluczowym problemem jest to, że produkt końcowy musi być tak dobry, jak to możliwe, przy minimalnym koszcie. Klient nie będzie dbał i nie będzie płacił za rzeczy, których nie widzi ani nie rozumie.

Chyba że zakodowałeś dla badań lub dla zabawy.

TL; DR: Twoja przyszła jaźń zawsze będzie przechytrzyć twoje obecne ja, więc nie zastanawiaj się nad tym. Wystarczy wybrać starannie roztwór roboczy, opanowania go i trzymać się go, aż nie rozwiąże swoje problemy: D

Aby odpowiedzieć na Twoje pytania:

  1. Kod jest w porządku, ale im więcej będzie staraj się, aby był "sprytny", "abstrakcyjny" lub "wolny od zależności", tym bardziej będziesz skłaniał się ku ORM.

  2. To, czego chcesz w pierwszym bloku kodu, jest całkiem wykonalne. Przyjrzeć się, w jaki sposób Doctrine ORM działa, lub to bardzo prosty ORM podejście Zrobiłem kilka miesięcy temu na projekt weekend:

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

+0

Rozumiem całkowicie, co masz na myśli, dając klientowi najlepszy możliwy produkt przy minimalnych kosztach. Ale ponieważ jest to technika, której mogę używać w więcej niż jednym projekcie, i próbuję użyć jej w niektórych moich własnych projektach, które nie są związane czasem lub pieniędzmi, chciałbym dobrze zrozumieć to, chociaż prawdopodobnie za kilka miesięcy/lat zrobię coś zupełnie innego. – Pinetree

+0

ORM da ci wielki ból i żal w nieco bardziej złożonym zastosowaniu niż podkład CD z kolekcji. Tyle minusów. – user151851

+1

Dwa lata później chciałbym móc powtórzyć to i usunąć wszystkie przegrane z moich odpowiedzi. : p – tuespetre

1

Oto inne podejście, o którym myślałem. Można utworzyć obiekt "DAOInjection", który działa jak kontener dla określonego obiektu DAO, wywołania zwrotnego i argumenty potrzebne do zwrócenia żądanych obiektów. Klasy muszą tylko wiedzieć o tej klasie DAOInjection, więc nadal są oddzielone od wszystkich Twoich 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; 
    } 
} 

Możesz również przekazać "InjectionHelper" jako argument. InjectionHelper działa jak kolejny kontener oddzwaniania - w ten sposób, jeśli potrzebujesz przekazać jakiekolwiek informacje o leniwym obiekcie ładującym do jego wstawionego DAO, nie będziesz musiał go zakodować na sztywno. Dodatkowo, jeśli potrzebujesz "potokować" metody razem - powiedz, że musisz przekazać $this->getDepartment()->getManager()->getId() do wstrzykniętego DAO z jakiegokolwiek powodu - możesz. Po prostu przekaż go jak getDepartment|getManager|getId do konstruktora 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; 
    } 
} 

celu realizacji tej funkcji w obiekcie, byś wymagają zastrzyki na budowie w celu zapewnienia, że ​​obiekt ma lub może uzyskać wszystkie potrzebne informacje. Każda metoda używająca wtrysku po prostu wywołuje metodę execute() odpowiedniego DAOInjection.

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

będę wtedy w konstruktorze moje klasy usług, instancję elementy odwzorowujące odpowiednich do tej usługi przy użyciu odpowiednich klas DAOInjection jako argumenty dla konstruktorów elementy odwzorowujące. Twórcy map będą wtedy dbać o to, aby każdy obiekt miał swoje iniekcje, ponieważ zadaniem odwzorowania jest zwracanie kompletnych obiektów i obsługa zapisywania/usuwania obiektów, podczas gdy zadaniem usługi jest koordynowanie relacji między różnymi elementami mapującymi, obiektami i tak. na.

Docelowo możesz użyć tego do wstrzykiwania wywołań zwrotnych do usług LUB twórców map, więc powiedz, że chcesz, aby Twój obiekt "Ticket" odzyskał użytkownika-rodzica, który znajduje się poza domeną usługi "Ticket Service" - usługa biletowa może po prostu wstawić wywołanie zwrotne do "usługi użytkownika" i nie musi wiedzieć nic o działaniu DAL dla innych obiektów.

Mam nadzieję, że to pomoże!