Devo usar Injeção de Dependência ou fábricas estáticas?

77

Ao projetar um sistema, sou frequentemente confrontado com o problema de ter um monte de módulos (log, acesso ao banco de dados, etc) sendo usados pelos outros módulos. A questão é: como faço para fornecer esses componentes para outros componentes? Duas respostas parecem possíveis de injeção de dependência ou usando o padrão de fábrica. No entanto, ambos parecem errados:

  • As fábricas dificultam o teste e não permitem a troca fácil de implementações. Eles também não tornam as dependências aparentes (por exemplo, você está examinando um método, alheio ao fato de que ele chama um método que chama um método que chama um método que usa um banco de dados).
  • A injeção de dependência aumenta enormemente as listas de argumentos do construtor e mancha alguns aspectos em todo o seu código. Situação típica é onde os construtores de mais de metade das classes se parecem com isso (....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

Aqui está uma situação típica com a qual tenho um problema: Eu tenho classes de exceção, que usam descrições de erro carregadas do banco de dados, usando uma consulta que possui o parâmetro de configuração de idioma do usuário, que está no objeto de sessão do usuário. Então, para criar uma nova exceção, eu preciso de uma descrição, que requer uma sessão de banco de dados e a sessão do usuário. Então, estou destinado a arrastar todos esses objetos por todos os meus métodos, para o caso de precisar lançar uma exceção.

Como eu lidei com esse problema?

    
por U Mad 11.03.2013 / 13:36
fonte

7 respostas

72

Use a injeção de dependência, mas sempre que as listas de argumentos do construtor se tornarem muito grandes, refatore-as usando uma Fachada Serviço . A ideia é agrupar alguns dos argumentos do construtor, introduzindo uma nova abstração.

Por exemplo, você pode introduzir um novo tipo SessionEnvironment encapsulando um DBSessionProvider , o UserSession e o carregado Descriptions . Para saber quais abstrações fazem mais sentido, no entanto, é preciso conhecer os detalhes do seu programa.

Uma pergunta semelhante já foi feita aqui no SO .

    
por 11.03.2013 / 13:57
fonte
19

Dependecy injection massively swells constructor argument lists and it smears some aspects all over your code.

A partir daí, parece que você não entende a DI propriamente dita - a idéia é inverter o padrão de instanciação de objetos dentro de uma fábrica.

Seu problema específico parece ser um problema mais geral de POO. Por que os objetos não podem simplesmente lançar exceções normais, não legíveis por humanos durante seu tempo de execução, e então ter algo antes do try / catch final que captura essa exceção e, nesse ponto, usa as informações da sessão para lançar uma exceção nova e mais bonita? ?

Outra abordagem seria ter uma fábrica de exceções, que é passada para os objetos por meio de seus construtores. Em vez de lançar uma nova exceção, a turma pode usar um método da fábrica (por exemplo, throw PrettyExceptionFactory.createException(data) .

Tenha em mente que seus objetos, além de seus objetos de fábrica, nunca devem usar o operador new . As exceções são geralmente o caso especial, mas no seu caso elas podem ser uma exceção!

    
por 11.03.2013 / 14:30
fonte
12

Você já listou bastante bem as desvantagens do padrão de fábrica estática, mas não concordo totalmente com as desvantagens do padrão de injeção de dependência:

Essa injeção de dependência requer que você escreva código para cada dependência não é um bug, mas um recurso: força você a pensar se realmente precisa dessas dependências, promovendo assim um acoplamento flexível. No seu exemplo:

Here's a typical situation I have a problem with: I have exception classes, which use error descriptions loaded from the database, using a query which has parameter of user language setting, which is in user session object. So to create a new Exception I need a description, which requires a database session and the user session. So I'm doomed to dragging all these objects across all my methods just in case I might need to throw an exception.

Não, você não está condenado. Por que é responsabilidade da lógica de negócios localizar suas mensagens de erro para uma sessão de usuário específica? E se, em algum momento no futuro, você quisesse usar esse serviço de negócios a partir de um programa em lote (que não possui uma sessão de usuário ...)? Ou se a mensagem de erro não deve ser mostrada para o usuário atualmente logado, mas seu supervisor (quem pode preferir um idioma diferente)? Ou se você quiser reutilizar a lógica de negócios no cliente (que não tem acesso a um banco de dados ...)?

Claramente, a localização de mensagens depende de quem olha essas mensagens, ou seja, é responsabilidade da camada de apresentação. Portanto, eu lançaria exceções comuns do serviço de negócios, que por acaso carregam um identificador de mensagem que pode então ser pesquisado no manipulador de exceção da camada de apresentação em qualquer fonte de mensagens que ele use.

Dessa forma, você pode remover 3 dependências desnecessárias (UserSession, ExceptionFactory e provavelmente descrições), tornando seu código mais simples e mais versátil.

De modo geral, eu só usaria fábricas estáticas para coisas que você precisa de acesso onipresente, e que estão garantidamente disponíveis em todos os ambientes em que poderíamos querer executar o código (como o Logging). Para todo o resto, eu usaria uma injeção de dependência antiga.

    
por 11.03.2013 / 23:34
fonte
1

Use a injeção de dependência. O uso de fábricas estáticas é um emprego do Service Locator antipattern. Veja o trabalho seminal de Martin Fowler aqui - link

Se os argumentos do construtor se tornarem muito grandes e você não estiver usando um contêiner DI por todos os meios, escreva suas próprias fábricas para instanciação, permitindo que ele seja configurável, seja por XML ou vinculando uma implementação a uma interface.

    
por 11.03.2013 / 14:15
fonte
1

Eu também iria com injeção de dependência. Lembre-se que o DI não é feito apenas através de construtores, mas também através de setters de propriedades. Por exemplo, o logger pode ser injetado como uma propriedade.

Além disso, você pode querer usar um contêiner IoC que pode aliviar parte da carga para você, por exemplo, mantendo os parâmetros do construtor em coisas que são necessárias em tempo de execução por sua lógica de domínio (mantendo o construtor de uma forma que revele a intenção da classe e as dependências reais do domínio) e talvez injetar outras classes auxiliares por meio de propriedades.

Um passo adiante você pode querer ir ao Programa Orientado a Aspectos, que é implementado em muitos frameworks importantes. Isso pode permitir que você intercepte (ou "aconselhe" a usar a terminologia do AspectJ) o construtor da classe e injete as propriedades relevantes, talvez com um atributo especial.

    
por 11.03.2013 / 14:42
fonte
0

Factories make testing a pain and don't allow easy swapping of implementations. They also don't make dependencies apparent (e.g. you're examining a method, oblivious to the fact that it calls a method that calls a method that calls a method that uses a database).

Eu não concordo totalmente. Pelo menos não em geral.

Fábrica simples:

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

Injeção simples:

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

Ambos os snippets têm o mesmo propósito, eles criam um link entre IFoo e Foo . Tudo o resto é apenas sintaxe.

A alteração de Foo para ADifferentFoo exige o mesmo esforço em qualquer exemplo de código.
Eu ouvi pessoas argumentarem que a injeção de dependência permite que diferentes ligações sejam usadas, mas o mesmo argumento pode ser feito sobre a fabricação de diferentes fábricas. Escolher a ligação certa é exatamente tão complexo quanto escolher a fábrica certa.

Os métodos de fábrica permitem, por exemplo, use Foo em alguns lugares e ADifferentFoo em outros lugares. Alguns podem chamar isso de bom (útil se você precisar), alguns podem chamar isso de ruim (você poderia fazer um trabalho meio-abusado em substituir tudo). No entanto, não é tão difícil evitar essa ambigüidade se você mantiver um único método que retorna IFoo para que você sempre tenha uma única fonte. Se você não quer se atirar no pé, não segure uma arma carregada, ou certifique-se de não apontar para o seu pé.

Dependecy injection massively swells constructor argument lists and it smears some aspects all over your code.

É por isso que algumas pessoas preferem recuperar explicitamente dependências no construtor, assim:

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

Eu já ouvi argumentos pro (sem construtor), eu ouvi argumentos con (usando o construtor permite mais automação DI).

Pessoalmente, embora eu tenha cedido ao nosso senior que deseja usar argumentos de construtor, notei um problema com a lista suspensa no VS (canto superior direito, para procurar os métodos da classe atual) onde os nomes dos métodos estão fora de visão quando uma das assinaturas do método é maior do que a minha tela (= > o construtor inchado).

Em um nível técnico, não me importo de nenhuma maneira. Qualquer opção leva tanto esforço para digitar. E desde que você está usando DI, você normalmente não vai chamar um construtor manualmente de qualquer maneira. Mas o erro da interface do Visual Studio faz-me favorecer o inchaço do argumento do construtor.

Como uma nota lateral, injeção de dependência e fábricas não são mutuamente exclusivas . Eu tive casos em que, em vez de inserir uma dependência, inseri uma fábrica que gera dependências (o NInject, felizmente, permite que você use Func<IFoo> para que você não precise criar uma classe de fábrica real).

Os casos de uso para isso são raros, mas eles existem.

    
por 31.07.2018 / 14:41
fonte
0

Neste exemplo simulado, uma classe de fábrica é usada em tempo de execução para determinar que tipo de objeto de solicitação HTTP de entrada instanciar, com base no método de solicitação HTTP. A própria fábrica é injetada com uma instância de um contêiner de injeção de dependência. Isso permite que a fábrica faça sua determinação de tempo de execução e deixe o contêiner de injeção de dependência manipular as dependências. Cada objeto de solicitação HTTP de entrada tem pelo menos quatro dependências (superglobais e outros 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.");
        }
    }
}

O código do cliente para uma configuração do tipo MVC, dentro de um index.php centralizado, pode parecer com o seguinte (validações omitidas).

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

Como alternativa, você pode remover a natureza estática (e a palavra-chave) da fábrica e permitir que um injetor de dependência gerencie tudo (portanto, o construtor comentado). No entanto, você terá que alterar algumas (não as constantes) das referências de membros da classe ( self ) para membros da instância ( $this ).

    
por 31.07.2018 / 14:20
fonte