2012-03-29 18 views
18

Recientemente comencé a leer acerca de la inyección de dependencia y me ha hecho repensar algunos de mis diseños.PHP Objetos de carga diferida e inyección de dependencia

El problema que tengo es algo como esto: Digamos que tengo dos clases: Coche y Pasajero; Para esas dos clases Tengo algunos creadores de mapas de datos para trabajar con la base de datos: CarDataMapper y PassengerDataMapper

Quiero ser capaz de hacer algo como esto en código:

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

Antes de saber nada acerca de DI, me gustaría construir mis clases de la siguiente manera:

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

} 

me gustaría también tienen un código similar en el método de pasajeros transportados> getCar() para buscar el coche que el pasajero se encuentra en

.

Ahora entiendo que esto crea dependencias (bueno, también lo entendí antes, pero no sabía que esto es "incorrecto") entre los objetos del Automóvil y del Pasajero y los objetos del asignador de datos.

Al tratar de pensar en la solución de estas dos opciones de vino a la mente, pero no me gusta ninguno de ellos:

1: Hacer algo como esto:

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

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

Pero lo si los pasajeros tienen objetos que necesitan inyectar, y qué pasa si el anidamiento va a diez o veinte niveles ... terminaría la creación de instancias de casi todos los objetos al inicio de mi aplicación, lo que a su vez consultaría toda la base de datos durante el proceso. Si tengo que enviar al pasajero a otro objeto que tiene que hacer algo con los objetos que tiene el pasajero, no quiero instanciar inmediatamente estos objetos también.

2: La inyección de los creadores de mapas de datos en los objetos de automóviles y pasajeros y tener algo como esto:

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

No me gusta esta mejor, ya que no es como el coche es realmente conscientes del asignador de datos, y sin el mapeador de datos, el auto podría comportarse de manera impredecible (no devolver pasajeros, cuando realmente los tiene)

Así que mi primera pregunta es: ¿Estoy tomando un enfoque completamente erróneo aquí, porque, cuanto más lo miro? , cuanto más parece que estoy construyendo un ORM, en lugar de una capa de negocios?

La segunda pregunta es: ¿Hay alguna manera de desacoplar realmente los objetos y los correladores de datos de una manera que me permita usar los objetos como se describe en el primer bloque de códigos?

Tercera pregunta: ¿ he visto algunas respuestas para otros idiomas (alguna versión de C, creo) la solución de este problema con algo como esto se describe aquí: What is the proper way to inject a data access dependency for lazy loading? Como no he tenido tiempo para jugar con otra idiomas, esto no tiene sentido para mí, así que estaría agradecido si alguien explicara los ejemplos en el enlace en PHP-ish.

También he mirado algunos marcos DI, y he leído sobre DI Containers e Inversion of Control, pero por lo que entendí se usan para definir e insertar dependencias para clases 'no dinámicas', donde por ejemplo, el Auto dependa del motor, pero no necesitaría que el motor se cargue dinámicamente desde el DB, simplemente se crearía una instancia y se inyectaría en el automóvil.

Perdón por la larga publicación y gracias de antemano.

+2

Es posible que desee realizar la compra [PoEAA] (http://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420/ref=sr_1_1?ie=UTF8&qid=1350785602&sr=8-1&keywords= patrones + de + empresa + aplicación + arquitectura) Aunque no ayudará en absoluto con [DI] (http://martinfowler.com/articles/injection.html) (es anterior a lo común) se dará cuenta de que incluso con su Business Layer necesitarás un ORM que se encuentre debajo de él. Se recomienda un [Data Mapper] (http://martinfowler.com/eaaCatalog/dataMapper.html). +1 por hacer la pregunta que he estado buscando. – xenoterracide

+0

Gracias por el consejo.Recientemente he estado usando Doctrine2 en la mayoría de mis proyectos, que es un ORM de Data Mapper, y me resulta bastante fácil trabajar con él. – Pinetree

Respuesta

5

Iba a decir "Sé que esto es una vieja pregunta, pero ... "luego me di cuenta de que la publicaste hace 9 horas, lo cual es genial, porque llegué a una 'resolución' satisfactoria para mí. Pensé en la implementación y luego me di cuenta de que es lo que la gente llamaba 'inyección de dependencia'.

Aquí se muestra un ejemplo:

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

} 

Luego, en su capa de servicios (donde 'coordinar' acciones entre múltiples creadores de mapas y modelos), puede llamar al método 'setReplyFetcher' en todos los objetos de entradas antes los devuelve a lo que está invocando la capa de servicio - O - podría hacer algo muy similar con cada asignador, otorgando al mapeador una propiedad privada 'fetcherInstance' y 'callback' para cada mapeador que el objeto va a necesitar, y luego configure ESO en la capa de servicio, luego el mapeador se encargará de preparar los objetos. Todavía estoy sopesando las diferencias entre los dos enfoques.

Ejemplo de coordinar en la capa de servicio:

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

cualquier manera que vaya, las clases de objetos son independientes de las clases mapper, y las clases mapper son independientes entre sí.

EDIT: He aquí un ejemplo de la otra manera de hacerlo:

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

Como se puede ver, el asignador requiere los otros recursos que estén disponibles incluso para crear una instancia. Como también puede ver, con este método está limitado a llamar a funciones de mapeador con identificadores de objetos como parámetros. Estoy seguro de que algunos se sientan y piensan que existe una solución elegante para incorporar otros parámetros, por ejemplo, buscar boletos "abiertos" en lugar de tickets "cerrados" que pertenecen a un objeto del departamento.

+0

Si entendí su respuesta correctamente, esto es similar a lo que tengo en la solución número 2 (inyectando los mapeadores de datos en los objetos), con la diferencia de que llamo al método mapeador directamente (creando una dependencia donde el objeto tiene que "saber" qué método llamar, incluso si el asignador está inyectado), y lo llama usando un valor de cadena inyectado como una devolución de llamada, lo que le daría cierta flexibilidad adicional. Solo estoy revisando aquí si entendí correctamente. – Pinetree

+0

Eso es correcto. De esta forma, el objeto no tiene que estar codificado con ninguna clase/instancia de correlacionador específica o devolución de llamada. – tuespetre

+0

Gracias por las respuestas aquí, ciertamente me hicieron pensar en lo que por ahora parece ser la dirección correcta. Sin embargo, lo he implementado de forma ligeramente diferente, usando mapeadores que tienen que ajustarse a una interfaz prescrita y objetos proxy en los que inyecte mapeadores para entidades relacionadas. – Pinetree

10

Tal vez fuera de tema, pero creo que le va a ayudar un poco:

Creo que intenta lograr la solución perfecta. Pero no importa lo que se te ocurra, en un par de años, tendrás más experiencia y definitivamente podrás mejorar tu diseño.

En los últimos años con mis colegas habíamos desarrollado muchos ORM/modelos comerciales, pero para casi todos los proyectos nuevos comenzábamos desde cero, ya que todos tenían más experiencia, todos habían aprendido de los errores anteriores y todos se habían encontrado con nuevos patrones e ideas. Todo eso agregó un mes adicional en desarrollo, lo que aumentó el costo del producto final.

No importa qué tan buenas sean las herramientas, el problema clave es que el producto final debe ser lo mejor posible, al costo mínimo. Al cliente no le importará y no pagará por cosas que no puede ver o entender.

A menos que, por supuesto, codifique para investigación o para diversión.

TL; DR: Tu ser futuro siempre será más astuto que tu yo actual, así que no lo pienses demasiado. Sólo tiene que elegir cuidadosamente una solución de trabajo, dominarla y aferrarse a él hasta que no se va a resolver sus problemas: D

para responder a sus preguntas:

  1. Su código está perfectamente bien, pero cuanto más se quiere trate de hacerlo "inteligente" o "abstracto" o "libre de dependencia", más se inclinará hacia un ORM.

  2. Lo que quiere en el primer bloque de código es bastante factible. Echar un vistazo a cómo funciona el ORM de Doctrine, o este enfoque ORM muy simple que hice hace unos meses para un proyecto de fin de semana:

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

+0

Entiendo completamente lo que quiere decir al darle al cliente el mejor producto posible al costo mínimo. Pero como esta es una técnica que probablemente usaré en más de un proyecto, y estoy tratando de usarla en algunos de mis proyectos que no están limitados por el tiempo o el dinero, me gustaría obtener una buena comprensión de aunque posiblemente haga algo completamente diferente en unos pocos meses/años. – Pinetree

+0

Un ORM le causará un gran dolor y dolor en una aplicación algo más compleja que una cartilla de colección de CD. Tantos contras. – user151851

+1

Dos años después, desearía poder volver a votar esto y eliminar todas las respuestas de mis respuestas. : p – tuespetre

1

Aquí hay otro enfoque en el que pensé. Puede crear un objeto 'DAOInjection' que actúe como un contenedor para el DAO específico, la devolución de llamada y los argumentos necesarios para devolver los objetos deseados. Las clases solo necesitan saber sobre esta clase DAOInjection, por lo que todavía están desacopladas de todos sus DAO/mapeadores/servicios/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; 
    } 
} 

También puede pasar un 'InjectionHelper' como argumento. El InjectionHelper actúa como otro contenedor de devolución de llamada: de esta manera, si necesita pasar cualquier información sobre el objeto de carga diferida a su DAO inyectado, no tendrá que codificarlo en el objeto. Además, si necesita 'pipetear' métodos juntos, digamos que necesita pasar $this->getDepartment()->getManager()->getId() al DAO inyectado por cualquier razón, puede hacerlo. Simplemente páselo como getDepartment|getManager|getId al constructor de 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; 
    } 
} 

para implementar esta funcionalidad en el objeto, que requeriría la inyección en su construcción para asegurar que el objeto tiene o puede obtener toda la información que necesita. Cada método que usa una inyección simplemente llama al método execute() de la respectiva 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; 
    } 
} 

Entonces yo, en el constructor de mi clase de servicio, crear instancias de los creadores de mapas correspondientes al servicio mediante las clases DAOInjection relevantes como argumentos para los constructores aplicadores. Los mapeadores se encargarían de asegurarse de que cada objeto tenga sus inyecciones, porque el trabajo del mapeador es devolver objetos completos y manejar el guardado/borrado de objetos, mientras que el trabajo del servicio es coordinar las relaciones entre varios mapeadores, objetos, y así en.

En última instancia, puede usarla para inyectar devoluciones de llamada a servicios O cartógrafos, por ejemplo, quiere que su objeto 'Ticket' recupere un usuario principal, que está fuera del 'Ticket Service' - el servicio de tickets solo puede insertar una devolución de llamada al "Servicio de usuario", y no tendrá que saber nada sobre cómo funciona el DAL para otros objetos.

Espero que esto ayude!

Cuestiones relacionadas