inyeccion injection explicacion ejemplo dependency dependencias control php oop dependency-injection

php - injection - Dependencia anulada en lugar de inyección de dependencia?



inyeccion de dependencias php (3)

Prefacio la pregunta larga con la versión corta de la pregunta:

Versión corta de la pregunta

¿Qué hay de malo en permitir que un objeto instancia sus propias dependencias, y luego proporcionar argumentos de constructor (o métodos setter) para simplemente anular las instancias predeterminadas?

class House { protected $door; protected $window; protected $roof; public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null) { $this->door = ($door) ? $door : new Door; $this->window = ($window) ? $window : new Window; $this->roof = ($roof) ? $roof : new Roof; } }

Versión larga de la pregunta

Mi motivación para esta pregunta es que la inyección de dependencia requiere que saltes a través de los aros solo para darle a un objeto lo que necesita. Contenedores de IoC, fábricas, localizadores de servicios ... todos ellos introducen muchas clases y abstracciones adicionales que complican la API de su aplicación, y yo argumentaría que las pruebas son igual de difíciles en muchos casos.

¿No es lógico que un objeto , de hecho, sepa qué dependencias necesita para funcionar correctamente?

Si las dos motivaciones principales de las inyecciones de dependencia son la capacidad de reutilización del código y la capacidad de prueba de la unidad, entonces poder anular las instancias predeterminadas con stubs u otros objetos lo hace muy bien.

Mientras tanto, si necesita agregar una clase House a su aplicación, SÓLO necesita codificar la clase House, y no una fábrica y / o un contenedor DI en la parte superior. Además, cualquier código de cliente que haga uso de la casa puede incluir la casa, y no necesita una fábrica de vivienda o un localizador de servicio abstracto desde arriba. Todo se vuelve extremadamente sencillo, sin código de intermediario, e instanciado solo cuando es necesario.

¿Estoy totalmente fuera de lugar al pensar que si un objeto tiene dependencias, debería poder cargarlas por sí mismo, a la vez que proporciona un mecanismo para que esas dependencias se sobrecarguen si se desea?

Ejemplo

#index.php (front controller) $db = new PDO(...); $cache = new Cache($dbGateway); $session = new Session($dbGateway); $router = new Router; $router::route(''/some/route'', function() use ($db, $cache, $session) { $controller = new SomeController($db, $cache, $session); $controller->doSomeAction(); }); #SomeController.php class SomeController { protected $db; protected $cache; protected $session; public function __construct(PDO $db, ICache $cache, ISession $session) { $this->db = $db; $this->cache = $cache; $this->session = $session; } public function doSomeAction() { $user = new /Domain/User; $userData = new /Data/User($this->db); $user->setName(''Derp''); $userData->save($user); } }

Ahora, en una aplicación muy grande con muchos modelos / clases de datos y controladores diferentes, tengo ganas de pasar el objeto DB atraves de cada controlador individual (que no lo necesita) solo para dárselo a todos los mapeadores de datos (que necesitarán it), es un poco apestoso.

Y, por extensión, pasar un localizador de servicio o un contenedor DI a través del controlador, solo para ubicar la base de datos y luego entregarla al mapeador de datos cada vez, también parece un poco apestoso.

Lo mismo vale para pasar una fábrica o una fábrica abstracta al controlador, y luego tener que instanciar objetos nuevos a través de algo engorroso como $this->factory->make(''/Data/User''); parece incomodo Especialmente dado que necesita codificar la clase de fábrica abstracta, entonces la fábrica real que conecta las dependencias para el objeto que desea.


Su pregunta está muy bien planteada y me gusta mucho que la gente cuestione cosas que tienen sentido común por razones de ''pruebas unitarias y capacidad de mantenimiento'' (no importa cuál de estos programas sea "un mal programador si no lo hace" -it -topics, siempre se trata de pruebas unitarias y mantenibilidad). Entonces, usted está haciendo la pregunta correcta aquí: ¿DI realmente respalda las pruebas unitarias y la capacidad de mantenimiento y, en caso afirmativo, cómo ? Y para anticiparlo: lo hace si se usa correctamente ...

Acerca de la descomposición

La Inyección de Dependencia (DI) y la Inversión de Control (IoC) son mecanismos que mejoran los conceptos básicos de encapsulación y separación de preocupaciones de OOP. Entonces, para responder a la pregunta, debe argumentarse por qué la encapsulación y la separación de preocupaciones son cosas geniales. Ambos son los principales mecanismos de descomposición: encapsulación (sí, tenemos módulos) y separación de preocupaciones (y tenemos módulos de una manera que tiene sentido). Se podría escribir mucho sobre este tema, pero, por ahora, debe ser suficiente decir que se trata de reducir la complejidad. La descomposición de un sistema le permite descomponer un sistema, sin importar cuán grande sea, en pedazos que un cerebro humano puede administrar. Aunque es un poco filosófico, eso es realmente importante: si no hubiera limitaciones del cerebro humano, el tema de la mantenibilidad no sería tan importante. Ok, entonces digamos: la descomposición es un truco para reducir la complejidad percibida de un sistema en pedazos que podemos gestionar.

Pero, como siempre, tiene un costo: la descomposición también agrega complejidad, como ha dicho con respecto a DI. Entonces, ¿todavía tiene sentido? Sí porque:

La complejidad añadida artificialmente es independiente de la complejidad inherente del sistema.

Eso es básicamente eso, en un nivel abstracto. Y tiene implicaciones: debe elegir el grado de descomposición y el esfuerzo que gasta para lograrlo, de acuerdo con la complejidad inherente del sistema que está construyendo (o la complejidad que puede alcanzar algún día).

Descomposición con DI

En particular, en lo que respecta a DI: de acuerdo con lo anterior, existen sistemas suficientemente pequeños, donde la complejidad añadida de DI no justifica la complejidad percibida reducida. Y, desafortunadamente, cada tutorial en la web se ocupa de uno de estos, lo que no permite entender de qué se trata todo esto.

Sin embargo, la mayoría (o muchos, al menos) de los proyectos de la vida real alcanzan un grado de complejidad inherente que la inversión en descomposición adicional se utiliza bien, porque la reducción de la complejidad percibida acelera el desarrollo posterior y reduce los errores. Y la inyección de dependencia es una de las técnicas para hacerlo:

DI admite la separación de qué (interfaz) y de cómo (implementación): si solo se trata de puertas de vidrio, estoy de acuerdo: si eso es demasiado para el cerebro de uno, probablemente no sea un programador. Pero las cosas son más complejas en la vida real: DI te permite enfocarte en lo que es realmente importante: como casa, no me importa mi puerta, siempre y cuando pueda confiar en el hecho de que se puede cerrar y abrir. Tal vez no hay puertas existentes en este momento? Simplemente no necesita preocuparse en este punto. Al registrar los componentes en su contenedor, puede enfocar nuevamente: ¿Qué puerta quiero en mi casa? Ya no necesita preocuparse por la puerta o la casa: están bien, ya lo sabe. Has separado las preocupaciones: la definición de cómo van las cosas juntas (componentes) y, en realidad, las juntas (contenedor). Eso es todo, por lo que puedo decir de mi experiencia. Suena torpe, pero en la vida real, es un gran logro.

Un poco menos filosófico

Para volver a bajar a la tierra, una serie de ventajas más prácticas:

Mientras un sistema está evolucionando, siempre hay partes que aún no se han desarrollado. Especificar un comportamiento es mucho menos trabajo que implementarlo, en la mayoría de los casos. Sin DI, no puede desarrollar su casa mientras no se haya desarrollado ninguna puerta, porque no hay nada para crear una instancia. Con DI, no te importa: diseñas tu casa, solo con las interfaces, escribes pruebas con burlas para estas interfaces y tu multa: tu casa funciona, sin ventanas ni puertas, incluso existentes.

Probablemente sepa lo siguiente: ha trabajado durante días en algo (digamos una puerta de vidrio) y está orgulloso. Seis meses después, mientras tanto, has aprendido mucho, lo vuelves a mirar y es una mierda. Lo tiras. Sin DI, necesita cambiar su casa, porque usa la clase que acaba de destruir. Con DI, tu casa no cambia. Puede sentarse en su propio ensamblaje: ni siquiera necesita recompilar el ensamblaje de la casa, no se toca. Esto, en un escenario complejo, es una gran ventaja.

Hay más, pero tal vez con todo esto en mente, se hace más fácil imaginar los beneficios de DI cuando lees sobre ellos la próxima vez ...


Un problema con hacer cosas como esta viene cuando tus dependencias también tienen dependencias que deben ser especificadas. Entonces su constructor necesita saber cómo construir sus dependencias y luego su constructor comienza a ser muy complicado.

Usando su ejemplo: El objeto Roof requiere un ángulo de cabeceo. El ángulo predeterminado depende de la ubicación de su casa (un techo plano no funciona tan bien con 10 ''de nieve) por cambios nuevos o cambios a las reglas comerciales. Así que ahora su House necesita calcular qué ángulo debe pasar al Roof . Puede hacer esto pasando la ubicación (que actualmente House solo necesita para calcular el ángulo o está creando una ubicación "predeterminada" para pasar en el constructor Roof ). De cualquier manera, el constructor ahora tiene que trabajar para crear el techo predeterminado.

Esto puede suceder con cualquiera de sus dependencias, una vez que una de ellas requiere que se determine / calcule algo, entonces su objeto debe conocer sus dependencias y cómo hacerlas. Algo que no debería tener que hacer.

Esto no sucederá necesariamente en todos los casos y, en algunos casos, podrá salirse con la suya. Sin embargo, corres un riesgo.

Intentar que las cosas sean "más fáciles" para las personas puede hacer que su diseño se vuelva inflexible y difícil ya que el código debe cambiarse.


Mientras que otras respuestas son buenas, trataré de enfocar esta pregunta desde un punto de vista práctico.

Imagínese, usted tiene un Sistema de gestión de contenido , donde puede modificar su configuración como lo desee. Supongamos que esta configuración se almacena en una base de datos. Y desde entonces, implica que debiste haber instanciado la cosa como:

$dsn = ''....''; $pdo = new PDO($dsn, $params); $config_adapter = new MySQL_Config_Adapter($pdo); $config_manager = new Config_Manager($config_adapter); // $config_manager is ready to be used

Ahora, veamos qué sucede si permitimos que una clase instancia sus propias dependencias

class Foo { public function __construct($config = null) { if ($config !== null) { global $pdo; $config_adapter = new MySQL_Config_Adapter($pdo); $config_manager = new Config_Manager($config_adapter); $this->config = $config_manager; } else { // Ok, it was injected $this->config = $config; } } }

Aquí hay 3 problemas obvios:

  • Estado global

Entonces, básicamente decides si quieres que esto tenga un estado global o no. Si proporciona una instancia de $config , significa que no desea un estado global. De lo contrario, estás diciendo que sí quieres esto.

  • Apretado-acoplamiento

Entonces, ¿qué pasa si decides cambiar de MySQL a MongoDB , o incluso una simple file-based PHP-array para almacenar CMS''s configuración CMS''s ? Entonces tendría que reescribir una gran cantidad de código, que es responsable de la inicialización de la dependencia.

  • Infracción no obvia del Principio de Responsabilidad Individual

Una clase debe tener solo una razón para cambiar. Una clase debe servir solo para un propósito singular. Eso significa que la clase Foo tiene más de una responsabilidad: también es responsable de la administración de la dependencia.

¿Cómo debería hacerse esto de forma adecuada?

public function __construct(IConfig $config) { $this->config = $config; }

Debido a que no está estrechamente acoplado a un adaptador particular, y desde entonces sería tan fácil probarlo en una unidad, o reemplazar un adaptador (Say, MySQL por otra cosa)

¿Qué hay sobre anular los argumentos predeterminados?

Si está anulando los objects predeterminados, entonces está haciendo algo mal, y eso es una señal de que su clase está haciendo demasiado.

Un propósito básico de los constructores es inicializar el estado de una clase. Si inicializó el estado, y luego está alterando ese estado a través de métodos de establecimiento de dependencia, entonces termina con una encapsulación defectuosa , que establece que un objeto debe tener el control completo de su estado e implementación.

Volver a los ejemplos de tu código

Veamos tu ejemplo de código.

public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null) { $this->door = ($door) ? $door : new Door; $this->window = ($window) ? $window : new Window; $this->roof = ($roof) ? $roof : new Roof; }

Aquí está diciendo algo como esto: si no se proporcionan algunos de los argumentos, entonces importe una instancia de ese argumento del alcance global . El problema aquí es que su House sabe de dónde vendrían las dependencias, mientras que debería ser completamente inconsciente de dicha información.

Ahora planteemos algunas preguntas sobre el escenario del mundo real:

  • ¿Qué pasa si quieres cambiar el color de tu puerta?
  • ¿Qué sucede si quiere cambiar el tamaño de su ventana?
  • ¿Qué sucede si desea usar la misma puerta para otra casa, pero con un tamaño de ventana diferente?

Si vas a seguir con la forma en que escribiste el código, entonces terminas con la duplicación del código masivo. Con DI "puro" en mente, esto será tan simple como:

$door = new Door(); $door->setColor(''black''); $window = new Window(); $window->setSize(500, 500); $a_house = new House($door, $window, $roof); // As I said, I want house2 to have the same door, but different window size $window->setSize(1000, 1000); $b_house = new House($door, $window, $roof);

OTRA VEZ: el punto central de Dependency Injection es que los objetos pueden compartir las mismas instancias

Una cosa más,

Los localizadores de servicios / contenedores de IoC son responsables del almacenamiento de objetos. Simplemente almacenan / recuperan objetos, como $pdo .

Las fábricas simplemente abstraen una instanciación de una clase.

Entonces, ellos no son "parte" de Inyección de Dependencia, se aprovechan de ello.

Eso es.