¿Debo usar inyección de dependencia o fábricas estáticas?

81

Al diseñar un sistema, a menudo me enfrento al problema de que los otros módulos utilicen un montón de módulos (registro, acceso a la base de datos, etc.). La pregunta es, ¿cómo hago para proporcionar estos componentes a otros componentes? Dos respuestas parecen posibles inyección de dependencia o utilizando el patrón de fábrica. Sin embargo, ambos parecen equivocados:

  • Las fábricas dificultan las pruebas y no permiten el intercambio fácil de implementaciones. Tampoco hacen aparentes las dependencias (por ejemplo, está examinando un método, ajeno al hecho de que llama a un método que llama a un método que llama a un método que utiliza una base de datos).
  • La inyección de dependencia aumenta enormemente las listas de argumentos del constructor y difumina algunos aspectos en todo el código. La situación típica es donde los constructores de más de medias clases se ven así(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Aquí hay una situación típica con la que tengo un problema: tengo clases de excepción, que usan descripciones de error cargadas de la base de datos, usando una consulta que tiene un parámetro de configuración de idioma de usuario, que está en el objeto de sesión de usuario. Entonces, para crear una nueva excepción, necesito una descripción, que requiere una sesión de base de datos y la sesión de usuario. Así que estoy condenado a arrastrar todos estos objetos a través de todos mis métodos en caso de que necesite lanzar una excepción.

¿Cómo abordo tal problema?

RokL
fuente
2
Si una fábrica puede resolver todos sus problemas, tal vez podría inyectar la fábrica en sus objetos y obtener LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession a partir de eso.
Giorgio
1
Demasiadas "entradas" a un método, ya sean pasadas o inyectadas, es más un problema del diseño de los métodos en sí. Independientemente de lo que elija, es posible que desee reducir un poco el tamaño de sus métodos (que es más fácil de hacer una vez que se aplica la inyección)
Bill K
La solución aquí no debería ser reducir los argumentos. En cambio, construya abstracciones que construyan un objeto de nivel superior que haga todo el trabajo en el objeto y le brinde beneficios.
Alex

Respuestas:

74

Utilice la inyección de dependencia, pero cada vez que las listas de argumentos de su constructor se vuelvan demasiado grandes, refactorícelas utilizando un Servicio de fachada . La idea es agrupar algunos de los argumentos del constructor, introduciendo una nueva abstracción.

Por ejemplo, podría introducir un nuevo tipo SessionEnvironmentencapsulando a DBSessionProvider, el UserSessiony el cargado Descriptions. Sin embargo, para saber qué abstracciones tienen más sentido, uno tiene que conocer los detalles de su programa.

Ya se hizo una pregunta similar aquí sobre SO .

Doc Brown
fuente
99
+1: Creo que agrupar argumentos de constructor en clases es una muy buena idea. También te obliga a organizar estos argumentos en más estructuras de significado.
Giorgio
55
Pero si el resultado no es una estructura significativa, entonces solo está ocultando la complejidad que viola SRP. En este caso, se debe realizar una refactorización de clase.
danidacar
1
@Giorgio No estoy de acuerdo con una afirmación general de que "agrupar argumentos de constructor en clases es una muy buena idea". Si calificas esto con "en este escenario", entonces es diferente.
tymtam
19

La inyección de dependencia aumenta enormemente las listas de argumentos del constructor y difumina algunos aspectos en todo el código.

A partir de eso, no parece que entiendas DI correctamente: la idea es invertir el patrón de creación de instancias de objetos dentro de una fábrica.

Su problema específico parece ser un problema OOP más general. ¿Por qué los objetos no pueden simplemente lanzar excepciones normales, no legibles por humanos durante su tiempo de ejecución, y luego tener algo antes del último intento / captura que atrapa esa excepción, y en ese punto usa la información de la sesión para lanzar una nueva excepción más bonita? ?

Otro enfoque sería tener una fábrica de excepciones, que se pasa a los objetos a través de sus constructores. En lugar de lanzar una nueva excepción, la clase puede lanzar un método de fábrica (p throw PrettyExceptionFactory.createException(data). Ej .

Tenga en cuenta que sus objetos, aparte de sus objetos de fábrica, nunca deben usar el newoperador. Las excepciones son generalmente el único caso especial, ¡pero en su caso podrían ser una excepción!

Jonathan Rich
fuente
1
He leído en alguna parte que cuando su lista de parámetros se hace demasiado larga no es porque esté usando inyección de dependencia sino porque necesita más inyección de dependencia.
Giorgio
Esa podría ser una de las razones: en general, dependiendo del lenguaje, su constructor no debería tener más de 6-8 argumentos, y no más de 3-4 de ellos deberían ser objetos, a menos que el patrón específico (como el Builderpatrón) lo dicta Si está pasando parámetros a su constructor porque su objeto crea instancias de otros objetos, ese es un caso claro para IoC.
Jonathan Rich
12

Ya ha enumerado las desventajas del patrón estático de fábrica bastante bien, pero no estoy del todo de acuerdo con las desventajas del patrón de inyección de dependencia:

Esa inyección de dependencias requiere que escribas código para cada dependencia no es un error, sino una característica: te obliga a pensar si realmente necesitas estas dependencias, promoviendo así un acoplamiento flexible. En tu ejemplo:

Aquí hay una situación típica con la que tengo un problema: tengo clases de excepción, que usan descripciones de error cargadas de la base de datos, usando una consulta que tiene un parámetro de configuración de idioma de usuario, que está en el objeto de sesión de usuario. Entonces, para crear una nueva excepción, necesito una descripción, que requiere una sesión de base de datos y la sesión de usuario. Así que estoy condenado a arrastrar todos estos objetos a través de todos mis métodos en caso de que necesite lanzar una excepción.

No, no estás condenado. ¿Por qué es responsabilidad de la lógica empresarial localizar sus mensajes de error para una sesión de usuario en particular? ¿Qué sucede si, en algún momento en el futuro, desea utilizar ese servicio comercial de un programa por lotes (que no tiene una sesión de usuario ...)? ¿O qué pasa si el mensaje de error no debe mostrarse al usuario actualmente conectado, sino a su supervisor (que puede preferir un idioma diferente)? ¿O qué pasaría si quisiera reutilizar la lógica empresarial en el cliente (que no tiene acceso a una base de datos ...)?

Claramente, la localización de mensajes depende de quién mira estos mensajes, es decir, es responsabilidad de la capa de presentación. Por lo tanto, lanzaría excepciones ordinarias del servicio comercial, que llevan un identificador de mensaje que luego se puede buscar en el controlador de excepciones de la capa de presentación en cualquier fuente de mensaje que utilice.

De esa manera, puede eliminar 3 dependencias innecesarias (UserSession, ExceptionFactory y probablemente descripciones), haciendo que su código sea más simple y versátil.

En términos generales, solo usaría fábricas estáticas para cosas a las que necesita acceso ubicuo, y que están garantizadas para estar disponibles en todos los entornos en los que podríamos querer ejecutar el código (como Logging). Para todo lo demás, usaría la inyección de dependencia simple y antigua.

meriton - en huelga
fuente
Esta. No puedo ver la necesidad de golpear el DB para lanzar una excepción.
Caleth
1

Usar inyección de dependencia. Usar fábricas estáticas es un empleo del Service Locatorantipatrón. Vea el trabajo seminal de Martin Fowler aquí: http://martinfowler.com/articles/injection.html

Si sus argumentos de constructor se vuelven demasiado grandes y no está utilizando un contenedor DI, escriba sus propias fábricas para la creación de instancias, permitiendo que sea configurable, ya sea por XML o vinculando una implementación a una interfaz.

Sam
fuente
55
Service Locator no es un antipatrón: el propio Fowler lo hace referencia en la URL que publicó. Si bien se puede abusar del patrón del Localizador de servicios (de la misma manera que se abusa de los Singletons, para abstraer el estado global), es un patrón muy útil.
Jonathan Rich
1
Interesante saberlo. Siempre lo he escuchado referido como un anti patrón.
Sam
44
Es solo un antipatrón si el localizador de servicios se utiliza para almacenar el estado global. El localizador de servicios debe ser un objeto sin estado después de la creación de instancias, y preferiblemente inmutable.
Jonathan Rich
XML no es de tipo seguro. Lo consideraría un plan z después de que cualquier otro enfoque haya fallado
Richard Tingle
1

Yo también iría con la inyección de dependencia. Recuerde que DI no solo se realiza a través de constructores, sino también a través de establecedores de propiedades. Por ejemplo, el registrador podría inyectarse como una propiedad.

Además, es posible que desee utilizar un contenedor de IoC que pueda levantar parte de la carga para usted, por ejemplo, manteniendo los parámetros del constructor en las cosas que su lógica de dominio necesita en tiempo de ejecución (manteniendo el constructor de una manera que revele la intención de la clase y las dependencias del dominio real) y tal vez inyectar otras clases auxiliares a través de propiedades.

Un paso más allá que tal vez quiera ir es el Programa orientado a aspectos, que se implementa en muchos marcos principales. Esto puede permitirle interceptar (o "aconsejar" usar la terminología de AspectJ) al constructor de la clase e inyectar las propiedades relevantes, tal vez con un atributo especial.

Tallmaris
fuente
44
Evito DI a través de setters porque introduce una ventana de tiempo durante el cual el objeto no está en estado completamente inicializado (entre el constructor y la llamada setter). O, en otras palabras, introduce un método de orden de llamada (debe llamar X antes de Y) que evito si es posible.
RokL
1
DI a través de establecedores de propiedades es ideal para dependencias opcionales El registro es un buen ejemplo. Si necesita iniciar sesión, configure la propiedad Logger; de lo contrario, no la configure.
Preston
1

Las fábricas dificultan las pruebas y no permiten el intercambio fácil de implementaciones. Tampoco hacen aparentes las dependencias (por ejemplo, está examinando un método, ajeno al hecho de que llama a un método que llama a un método que llama a un método que utiliza una base de datos).

No estoy del todo de acuerdo. Al menos no en general.

Fábrica simple:

public IFoo GetIFoo()
{
    return new Foo();
}

Inyección simple:

myDependencyInjector.Bind<IFoo>().To<Foo>();

Ambos fragmentos tienen el mismo propósito, establecen un enlace entre IFooy Foo. Todo lo demás es solo sintaxis.

Cambiar Fooa ADifferentFoorequiere exactamente el mismo esfuerzo en cualquiera de los ejemplos de código.
He escuchado a personas argumentar que la inyección de dependencia permite el uso de diferentes enlaces, pero se puede hacer el mismo argumento sobre la fabricación de diferentes fábricas. Elegir la encuadernación correcta es exactamente tan complejo como elegir la fábrica correcta.

Los métodos de fábrica le permiten, por ejemplo, usar Fooen algunos lugares y ADifferentFooen otros lugares. Algunos pueden llamar a esto bueno (útil si lo necesita), algunos pueden llamar a esto malo (podría hacer un trabajo a medias para reemplazar todo).
Sin embargo, no es tan difícil evitar esta ambigüedad si se adhiere a un único método que regresa IFoopara que siempre tenga una sola fuente. Si no quieres dispararte en el pie, no sostengas un arma cargada o asegúrate de no apuntar a tu pie.


La inyección de dependencia aumenta enormemente las listas de argumentos del constructor y difumina algunos aspectos en todo el código.

Esta es la razón por la cual algunas personas prefieren recuperar explícitamente dependencias en el constructor, así:

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

He escuchado argumentos pro (sin hincha del constructor), he escuchado argumentos con (el uso del constructor permite más automatización DI).

Personalmente, aunque me he rendido a nuestro superior que quiere usar argumentos de constructor, noté un problema con la lista desplegable en VS (arriba a la derecha, para examinar los métodos de la clase actual) donde los nombres de los métodos se pierden de vista cuando uno de las firmas de método es más largo que mi pantalla (=> el constructor hinchado).

A nivel técnico, no me importa de ninguna manera. Cualquiera de las opciones requiere tanto esfuerzo para escribir. Y dado que está utilizando DI, por lo general no llamará a un constructor manualmente. Pero el error de Visual Studio UI me hace favorecer no hinchar el argumento del constructor.


Como nota al margen, la inyección de dependencia y las fábricas no son mutuamente excluyentes . He tenido casos en los que, en lugar de insertar una dependencia, he insertado una fábrica que genera dependencias (afortunadamente, NInject te permite usar Func<IFoo>para que no necesites crear una clase de fábrica real).

Los casos de uso para esto son raros pero existen.

Flater
fuente
OP pregunta por las fábricas estáticas . stackoverflow.com/questions/929021/…
Basilevs
@Basilevs "La pregunta es, ¿cómo hago para proporcionar estos componentes a otros componentes".
Anthony Rutledge
1
@Basilevs Todo lo que dije, aparte de la nota al margen, también se aplica a las fábricas estáticas. No estoy seguro de lo que estás tratando de señalar específicamente. ¿A qué se refiere el enlace?
Flater
¿Podría uno de estos casos de uso inyectar una fábrica en el caso de que uno haya ideado una clase de solicitud HTTP abstracta y haya planeado otras cinco clases secundarias polimórficas, una para GET, POST, PUT, PATCH y DELETE? No puede saber qué método HTTP se utilizará cada vez que intente integrar dicha jerarquía de clases con un enrutador de tipo MVC (que puede depender en parte de una clase de solicitud HTTP. La interfaz de mensajes HTTP PSR-7 es fea.
Anthony Rutledge
@AnthonyRutledge: las fábricas DI pueden significar dos cosas (1) Una clase que expone múltiples métodos. Creo que esto es de lo que estás hablando. Sin embargo, esto no es realmente particular de las fábricas. Ya sea una clase de lógica de negocios con varios métodos públicos, o una fábrica con varios métodos públicos, es una cuestión de semántica y no hace ninguna diferencia técnica. (2) El caso de uso específico de DI para las fábricas es que una dependencia que no es de fábrica se instancia una vez (durante la inyección), mientras que una versión de fábrica se puede usar para crear una instancia de la dependencia real en una etapa posterior (y posiblemente varias veces).
Flater
0

En este ejemplo simulado, se usa una clase de fábrica en tiempo de ejecución para determinar qué tipo de objeto de solicitud HTTP entrante se debe instanciar, según el método de solicitud HTTP. La fábrica misma recibe una instancia de un contenedor de inyección de dependencia. Esto permite que la fábrica haga su determinación de tiempo de ejecución y permite que el contenedor de inyección de dependencias maneje las dependencias. Cada objeto de solicitud HTTP entrante tiene al menos cuatro dependencias (superglobales y otros objetos).

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

El código del cliente para una configuración de tipo MVC, dentro de un sistema centralizado index.php, podría tener el siguiente aspecto (se omiten las validaciones).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

Alternativamente, puede eliminar la naturaleza estática (y la palabra clave) de la fábrica y permitir que un inyector de dependencia administre todo (por lo tanto, el constructor comentado). Sin embargo, tendrá que cambiar algunas (no las constantes) de las referencias de miembros de clase ( self) a miembros de instancia ( $this).

Anthony Rutledge
fuente
Los votos negativos sin comentarios no son útiles.
Anthony Rutledge