inyeccion - PHP carga perezosa de objetos e inyección de dependencias.
inyeccion de dependencias php (3)
Recientemente empecé a leer sobre la inyección de dependencias y me ha hecho reconsiderar algunos de mis diseños.
El problema que tengo es algo como esto: Digamos que tengo dos clases: Automóvil y Pasajero; Para esas dos clases tengo algunos mapeadores de datos para trabajar con la base de datos: CarDataMapper y PassengerDataMapper
Quiero poder 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 algo sobre DI, construía mis clases de esta 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;
}
}
También tendría un código similar en el método Pasajero-> getCar () para buscar el automóvil en el que se encuentra el pasajero.
Ahora entiendo que esto crea dependencias (bueno, también lo entendí antes, pero no era consciente de que esto es "incorrecto") entre los objetos Automóvil y Pasajero y los objetos del asignador de datos.
Al intentar pensar en la solución para estas dos opciones, me vino a la mente, pero realmente no me gustan ninguna de ellas:
1: haciendo algo como esto:
$car = $carDataMapper->getCarById(23);
$passengers = $passengerDataMapper->getPassengersByCarId($car->getId());
$car->setPassengers($passengers);
foreach($car->getPassengers() as $passenger){
$passenger->doSomething();
}
Pero qué pasa si los pasajeros tienen objetos que necesitan inyectarse, y qué pasa si el agrupamiento llega a diez o veinte niveles ... Terminaría creando una instancia 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 crear una instancia inmediata de estos objetos también.
2: inyectar los asignadores 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 esto mejor, porque no es como si el Coche realmente no se diera cuenta del mapeador de datos, y sin el mapeador de datos, el Coche podría comportarse de manera impredecible (no los pasajeros que regresan, cuando en realidad los tiene)
Entonces, mi primera pregunta es: ¿Estoy tomando un enfoque completamente equivocado aquí porque, cuanto más lo veo, más parece que estoy creando un ORM, en lugar de una capa empresarial?
La segunda pregunta es: ¿hay una manera de realmente desacoplar los objetos y los mapeadores de datos de una manera que me permita usar los objetos como se describe en el primer bloque de código?
Tercera pregunta: he visto algunas respuestas para otros idiomas (una versión de C, creo) resolviendo este problema con algo como lo que se describe aquí: ¿Cuál es la forma correcta de inyectar una dependencia de acceso a datos para la carga diferida? Como no he tenido tiempo de jugar con otros idiomas, esto no tiene sentido para mí, así que agradecería que alguien explicara los ejemplos en el enlace en PHP-ish.
También he examinado algunos marcos DI y he leído sobre Contenedores DI e Inversión de control, pero por lo que entendí, se utilizan para definir e inyectar dependencias para clases "no dinámicas", donde, por ejemplo, el Coche dependería del Motor. , pero no necesitaría que el motor se cargue dinámicamente desde la base de datos, simplemente se crearía una instancia e inyectaría en el automóvil.
Lo siento por el largo post y gracias de antemano.
Aquí hay otro enfoque que pensé. Puede crear un objeto ''DAOInjection'' que actúa 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 acerca de esta clase de DAOInjection, por lo que aún están desconectadas de todos sus 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;
}
}
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 lenta a su DAO inyectado, no tendrá que codificarlo en el objeto. Además, si necesita ''canalizar'' métodos juntos, digamos que necesita pasar $this->getDepartment()->getManager()->getId()
a la DAO inyectada por cualquier motivo, puede $this->getDepartment()->getManager()->getId()
. 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, necesitaría las inyecciones en la construcción para asegurarse de que el objeto tenga o pueda obtener toda la información que necesita. Cada método que utiliza una inyección simplemente llama al método execute () de la DAOInjection respectiva.
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;
}
}
Luego, en el constructor de mi clase de servicio, crearía una instancia de los mappers relevantes para ese servicio utilizando las clases DAOInjection relevantes como argumentos para los constructores de los mappers. Los asignadores luego se encargarán de asegurarse de que cada objeto tenga sus inyecciones, ya que el trabajo del asignador es devolver los objetos completos y manejar el guardado / borrado de objetos, mientras que el trabajo del servicio es coordinar las relaciones entre varios asignadores, objetos, etc. en.
En última instancia, puede usarlo para inyectar devoluciones de llamadas a servicios O asignadores, así que diga que desea que su objeto ''Boleto'' recupere un usuario principal, que está fuera del ámbito del ''Servicio de boletos''; el servicio de boletos puede inyectar un devuelva la llamada al ''Servicio de usuario'', y no tendrá que saber nada sobre cómo funciona el DAL para otros objetos.
¡Espero que esto ayude!
Iba a decir: "Sé que esta es una pregunta antigua 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 era lo que la gente llamaba "inyección de dependencia".
Aquí hay 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 servicio (donde "coordina" las acciones entre varios mapeadores y modelos) puede llamar al método ''setReplyFetcher'' en todos los objetos del ticket antes de devolverlos a lo que sea que esté invocando la capa de servicio. podría hacer algo muy similar con cada asignador, dándole al asignador una propiedad privada ''fetcherInstance'' y ''devolución de llamada'' para cada asignador que el objeto va a necesitar, y luego configurar ESO en la capa de servicio, entonces el asignador se encargará de preparando los objetos. Todavía estoy sopesando las diferencias entre los dos enfoques.
Ejemplo de coordinación 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;
}
}
De cualquier manera, las clases de objetos son independientes de las clases de asignadores, y las clases de asignadores son independientes entre sí.
EDITAR: Aquí hay un ejemplo de la otra forma 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 puede ver, el asignador requiere que los otros recursos estén disponibles incluso para ser instanciados. 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, al sentarse y pensar, existe una solución elegante para incorporar otros parámetros, por ejemplo, obtener tickets ''abiertos'' en lugar de tickets ''cerrados'' que pertenecen a un objeto del departamento.
Tal vez fuera de tema, pero creo que te ayudará un poco:
Creo que intentas conseguir la solución perfecta. Pero no importa lo que surja, en un par de años, tendrá más experiencia y definitivamente podrá mejorar su diseño.
En los últimos años, con mis colegas hemos desarrollado muchos ORM / Modelos de negocio, pero para casi todos los proyectos nuevos comenzamos desde cero, ya que todos tenían más experiencia, todos habían aprendido de los errores anteriores y todos habían encontrado nuevos patrones y ideas Todo eso agregó un mes adicional más o menos en el 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 pueden ver o entender.
A menos que, por supuesto, codifiques para investigación o para diversión.
TL; DR: Tu futuro serás siempre más inteligente que tu yo actual, así que no pienses demasiado en ello. Simplemente elija cuidadosamente una solución de trabajo, domínela y siga con ella hasta que no resuelva sus problemas: D
Para responder tu pregunta:
Su código está perfectamente bien, pero cuanto más trate de hacerlo "inteligente" o "abstracto" o "sin dependencia", más se inclinará hacia un ORM.
Lo que quieras en el primer bloque de código es bastante factible. Eche 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: