Devo usar injeção de dependência ou fábricas estáticas?

81

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

  • As fábricas tornam o teste uma tarefa difícil 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 massivamente as listas de argumentos do construtor e mancha alguns aspectos em todo o seu código. Situação típica é onde construtores de mais da 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: Tenho classes de exceção, que usam descrições de erro carregadas do banco de dados, usando uma consulta com parâmetro de configuração do idioma do usuário, que está no objeto de sessão do usuário. Portanto, para criar uma nova exceção, preciso de uma descrição, que exija uma sessão de banco de dados e a sessão do usuário. Portanto, estou fadado a arrastar todos esses objetos em todos os meus métodos para o caso de eu precisar lançar uma exceção.

Como faço para resolver esse problema?

RokL
fonte
2
Se uma fábrica puder resolver todos os seus problemas, talvez você possa injetar a fábrica em seus objetos e obter LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession a partir disso.
Giorgio
1
Muitas "entradas" em um método, passadas ou injetadas, são mais um problema do próprio design dos métodos. O que quer que você escolha, poderá reduzir um pouco o tamanho dos seus métodos (o que é mais fácil de fazer quando você injeta a injeção) #
K Bill Bill K
A solução aqui não deve ser reduzir os argumentos. Em vez disso, crie abstrações que criam um objeto de nível superior que faz todo o trabalho no objeto e oferece benefícios.
Alex

Respostas:

74

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

Por exemplo, você pode introduzir um novo tipo que SessionEnvironmentencapsule a DBSessionProvider, the UserSessione 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 .

Doc Brown
fonte
9
+1: acho que agrupar argumentos de construtor em classes é uma ideia muito boa. Também obriga a organizar esses argumentos em mais estruturas de significado.
Giorgio
5
Mas se o resultado não for uma estrutura significativa, você estará ocultando a complexidade que viola o SRP. Nesse caso, uma refatoração de classe deve ser feita.
Danidacar 15/09/13
1
@ Giorgio Não concordo com uma afirmação geral de que "agrupar argumentos de construtores em classes é uma idéia muito boa". Se você qualificar isso como "neste cenário", será diferente.
tymtam
19

A injeção de dependência aumenta massivamente as listas de argumentos do construtor e mancha alguns aspectos em todo o seu código.

A partir disso, não parece que você entenda o DI adequadamente - 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 e não legíveis por humanos durante o tempo de execução e depois ter algo antes da tentativa / captura final que captura essa exceção e, nesse ponto, usa as informações da sessão para lançar uma nova exceção mais bonita ?

Outra abordagem seria ter uma fábrica de exceções, que é passada aos objetos por meio de seus construtores. Ao invés de jogar uma nova exceção, a classe pode jogar em um método de fábrica (por exemplo throw PrettyExceptionFactory.createException(data).

Lembre-se de que seus objetos, além dos objetos de fábrica, nunca devem usar o newoperador. As exceções geralmente são o único caso especial, mas no seu caso elas podem ser uma exceção!

Jonathan Rich
fonte
1
Li em algum lugar que, quando sua lista de parâmetros fica muito longa, não é porque você está usando injeção de dependência, mas porque precisa de mais injeção de dependência.
Giorgio
Essa pode ser uma das razões - geralmente, dependendo da linguagem, seu construtor não deve ter mais do que 6-8 argumentos, e não mais do que 3-4 deles devem ser objetos próprios, a menos que o padrão específico (como o Builderpadrão) dita. Se você está passando parâmetros para o construtor porque seu objeto instancia outros objetos, esse é um caso claro para a IoC.
Jonathan Rico
12

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

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

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

Não, você não está condenado. Por que é responsabilidade da lógica comercial localizar suas mensagens de erro para uma sessão de usuário específica? E se, em algum momento no futuro, você quiser usar esse serviço comercial de um programa em lote (que não possui uma sessão do usuário ...)? Ou, se a mensagem de erro não for exibida ao usuário conectado no momento, mas ao seu supervisor (que 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 as vê, ou seja, é de responsabilidade da camada de apresentação. Portanto, eu lançaria exceções comuns do serviço comercial, que carregam um identificador de mensagem que pode ser consultado no manipulador de exceção da camada de apresentação em qualquer fonte de mensagem que ele usar.

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

De um modo geral, eu usaria apenas fábricas estáticas para coisas às quais você precisa de acesso onipresente e que estão garantidas em todos os ambientes nos quais poderíamos querer executar o código (como Log). Para todo o resto, eu usaria injeção de dependência antiga simples.

meriton - em greve
fonte
Este. Não vejo a necessidade de acessar o banco de dados para lançar uma exceção.
Caleth
1

Use injeção de dependência. Usar fábricas estáticas é um emprego do Service Locatorantipadrão. Veja o trabalho seminal de Martin Fowler aqui - http://martinfowler.com/articles/injection.html

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

Sam
fonte
5
O Localizador de serviço não é um antipadrão - o próprio Fowler faz referência a ele no URL que você postou. Embora o padrão do Localizador de Serviço possa ser abusado (da mesma maneira que os Singletons são abusados ​​- para abstrair o estado global), é um padrão muito útil.
Jonathan Rico
1
Interessante saber. Eu sempre ouvi isso referido como um anti-padrão.
Sam
4
É apenas um antipadrão se o localizador de serviço for usado para armazenar o estado global. O localizador de serviço deve ser um objeto sem estado após instanciação e, de preferência imutável.
Jonathan Rico
XML não é do tipo seguro. Eu consideraria isso um plano z depois de cada outra abordagem falhou
Richard Tingle
1

Eu também aceitaria a injeção de dependência. Lembre-se de que o DI não é feito apenas por meio de construtores, mas também por meio de configuradores de propriedades. Por exemplo, o criador de logs pode ser injetado como uma propriedade.

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

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

Tallmaris
fonte
4
Eu evito o DI através de setters porque ele introduz uma janela de tempo durante a qual o objeto não está no estado totalmente inicializado (entre a chamada do construtor e a do setter). Ou, em outras palavras, introduz uma ordem de chamada de método (deve chamar X antes de Y) que eu evito, se possível.
RokL
1
A DI através de configuradores de propriedades é ideal para dependências opcionais. O log é um bom exemplo. Se você precisar fazer logon, defina a propriedade Logger, caso contrário, não a configure.
Preston
1

As fábricas tornam o teste uma tarefa difícil 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).

Eu não concordo completamente. 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 objetivo, eles estabelecem um link entre IFooe Foo. Tudo o resto é apenas sintaxe.

Alterar Foopara ADifferentFooexige exatamente o mesmo esforço em qualquer exemplo de código.
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 fábricas diferentes. Escolher a encadernação correta é exatamente tão complexo quanto escolher a fábrica certa.

Os métodos de fábrica permitem que você, por exemplo, use Fooem alguns lugares e ADifferentFooem outros lugares. Alguns podem chamar isso de bom (útil se você precisar), outros podem chamar isso de ruim (você pode fazer um trabalho de meia-boca para substituir tudo).
No entanto, não é tão difícil evitar essa ambiguidade se você se ater a um único método que retorna IFoopara que você sempre tenha uma única fonte. Se você não quer dar um tiro no pé, não segure uma arma carregada ou não a aponte para o pé.


A injeção de dependência aumenta massivamente as listas de argumentos do construtor e mancha alguns aspectos em todo o seu código.

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

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

Eu ouvi argumentos pro (nenhum construtor inchaço), eu ouvi argumentos con (usar o construtor permite mais automação de DI).

Pessoalmente, enquanto eu cedi ao nosso veterano que deseja usar argumentos construtores, notei um problema com a lista suspensa no VS (canto superior direito, para navegar pelos métodos da classe atual), onde os nomes dos métodos desaparecem quando um das assinaturas do método é maior que minha tela (=> o construtor inchado).

No nível técnico, eu não me importo de nenhuma maneira. Qualquer uma das opções requer o mesmo esforço para digitar. E como você está usando DI, normalmente não chama um construtor manualmente de qualquer maneira. Mas o bug da interface do usuário do Visual Studio me faz favor não inchar o argumento do construtor.


Como uma observação lateral, injeção de dependência e fábricas não são mutuamente exclusivas . Já tive casos em que, em vez de inserir uma dependência, inseri uma fábrica que gera dependências (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.

Flater
fonte
O OP pergunta sobre fábricas estáticas . stackoverflow.com/questions/929021/…
Basilevs
@Basilevs "A questão é: como faço para fornecer esses componentes a outros componentes."
Anthony Rutledge
1
@Basilevs Tudo o que eu disse, além da nota lateral, também se aplica às fábricas estáticas. Não tenho certeza do que você está tentando apontar especificamente. Qual é o link em referência?
Flater 31/07
Poderia um desses casos de uso de injeção de fábrica ser o caso em que se criou uma classe de solicitação HTTP abstrata e estrategizou outras cinco classes filho polimórficas, uma para GET, POST, PUT, PATCH e DELETE? Você não pode saber qual o método HTTP será usado cada vez que tentar integrar disse hierarquia de classes com um router Tipo MVC (que pode confiar, em parte, uma classe pedido HTTP PSR-7 HTTP Mensagem Interface é feio..
Anthony Rutledge
@AnthonyRutledge: fábricas de DI podem significar duas coisas (1) Uma classe que expõe vários métodos. Eu acho que é disso que você está falando. No entanto, isso não é realmente específico das fábricas. Seja uma classe de lógica de negócios com vários métodos públicos ou uma fábrica com vários métodos públicos, é uma questão de semântica e não faz diferença técnica. (2) O caso de uso específico de DI para fábricas é que uma dependência que não seja de fábrica é instanciada uma vez (durante a injeção), enquanto uma versão de fábrica pode ser usada para instanciar a dependência real em um estágio posterior (e possivelmente várias vezes).
Flater
0

Neste exemplo simulado, uma classe de fábrica é usada em tempo de execução para determinar qual 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 permita que o contêiner de injeção de dependência lide com as dependências. Cada objeto de solicitação HTTP de entrada possui 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 uma centralização index.php, pode se 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ê precisará alterar algumas (não as constantes) das referências dos membros da classe ( self) para os membros da instância ( $this).

Anthony Rutledge
fonte
Votos negativos sem comentários não são úteis.
Anthony Rutledge