Injeção de dependência e padrão de design singleton

92

Como identificamos quando usar injeção de dependência ou padrão singleton. Eu li em muitos sites onde eles dizem "Use injeção de dependência sobre o padrão singleton". Mas não tenho certeza se concordo totalmente com eles. Para meus projetos de pequena ou média escala, definitivamente vejo o uso do padrão singleton direto.

Por exemplo, Logger. Eu poderia usar Logger.GetInstance().Log(...) Mas, em vez disso, por que preciso injetar todas as classes que crio, com a instância do logger ?.

SysAdmin
fonte

Respostas:

67

Se você deseja verificar o que é registrado em um teste, você precisa de injeção de dependência. Além disso, um logger raramente é um único - geralmente você tem um logger para cada uma de sua classe.

Assista a esta apresentação sobre design orientado a objetos para testabilidade e você verá por que singletons são ruins.

O problema com os singletons é que eles representam um estado global difícil de prever, especialmente em testes.

Tenha em mente que um objeto pode ser um único de fato, mas ainda assim ser obtido por meio de injeção de dependência, em vez de por meio de Singleton.getInstance().

Estou apenas listando alguns pontos importantes feitos por Misko Hevery em sua apresentação. Depois de assisti-lo, você terá uma perspectiva completa de por que é melhor fazer um objeto definir quais são suas dependências, mas não definir uma maneira de criá- las.

Bozho
fonte
89

Singletons são como o comunismo: ambos parecem ótimos no papel, mas explodem em problemas na prática.

O padrão singleton coloca uma ênfase desproporcional na facilidade de acesso aos objetos. Ele evita completamente o contexto, exigindo que cada consumidor use um objeto com escopo AppDomain, sem deixar opções para implementações variadas. Ele incorpora conhecimento de infraestrutura em suas aulas (a chamada para GetInstance()) enquanto adiciona potência expressiva exatamente zero . Na verdade, diminui seu poder expressivo, porque você não pode alterar a implementação usada por uma classe sem alterá-la para todas elas. Você simplesmente não pode adicionar peças únicas de funcionalidade.

Além disso, quando a classe Foodepende de Logger.GetInstance(), Fooestá efetivamente ocultando suas dependências dos consumidores. Isso significa que você não pode entendê Foo-lo totalmente ou usá-lo com confiança, a menos que leia sua fonte e descubra o fato de que depende Logger. Se você não tem o código-fonte, isso limita quão bem você pode entender e usar efetivamente o código do qual depende.

O padrão singleton, conforme implementado com propriedades / métodos estáticos, é pouco mais do que um hack em torno da implementação de uma infraestrutura. Isso o limita de inúmeras maneiras, ao mesmo tempo que não oferece nenhum benefício perceptível sobre as alternativas. Você pode usá-lo como quiser, mas como existem alternativas viáveis ​​que promovem um design melhor, nunca deve ser uma prática recomendada.

Bryan Watts
fonte
9
@BryanWatts tudo o que disse, Singletons ainda são muito mais rápidos e menos propensos a erros em um aplicativo normal de média escala. Normalmente, não tenho mais de uma implementação possível para um registrador (personalizado), então por que ** eu precisaria: 1. Criar uma interface para isso e alterá-la sempre que precisar adicionar / alterar membros públicos 2. Manter uma configuração DI 3. Ocultar o fato de que existe um único objeto desse tipo em todo o sistema 4. Vincular-me a uma funcionalidade restrita causada por uma separação prematura de preocupações.
Uri Abramson
@UriAbramson: Parece que você já tomou a decisão sobre as compensações que prefere, então não vou tentar convencê-lo.
Bryan Watts
@BryanWatts Não é o caso, talvez meu comentário tenha sido um pouco áspero, mas na verdade me interessa muito ouvir o que você tem a dizer sobre o ponto que mencionei ...
Uri Abramson
2
@UriAbramson: Muito justo. Você pelo menos concorda que a troca de implementações por isolamento durante o teste é importante?
Bryan Watts
6
O uso de singletons e injeção de dependência não é mutuamente exclusivo. Um singleton pode implementar uma interface, portanto, pode ser usado para satisfazer uma dependência de outra classe. O fato de ser um singleton não força todo consumidor a obter uma referência por meio de seu método / propriedade "GetInstance".
Oliver
19

Outros explicaram muito bem o problema com os singletons em geral. Gostaria apenas de acrescentar uma observação sobre o caso específico do Logger. Concordo com você que geralmente não é um problema acessar um Logger (ou o logger root, para ser mais preciso) como um singleton, por meio de um método estático getInstance()ou getRootLogger(). (a menos que você queira ver o que é registrado pela classe que você está testando - mas, em minha experiência, dificilmente me lembro desses casos em que isso foi necessário. Novamente, para outros, essa pode ser uma preocupação mais urgente).

IMO geralmente um registrador de singleton não é uma preocupação, pois não contém nenhum estado relevante para a classe que você está testando. Ou seja, o estado do logger (e suas possíveis mudanças) não têm nenhum efeito sobre o estado da classe testada. Portanto, isso não torna seus testes de unidade mais difíceis.

A alternativa seria injetar o logger por meio do construtor em (quase) todas as classes em seu aplicativo. Para consistência de interfaces, deve ser injetado mesmo se a classe em questão não registrar nada no momento - a alternativa seria que quando você descobrir em algum ponto que agora você precisa registrar algo desta classe, você precisa de um logger, portanto você precisa adicionar um parâmetro de construtor para DI, quebrando todo o código do cliente. Não gosto de ambas as opções e sinto que usar DI para registro seria apenas complicar minha vida para cumprir uma regra teórica, sem nenhum benefício concreto.

Portanto, meu ponto principal é: uma classe que é usada (quase) universalmente, mas não contém o estado relevante para seu aplicativo, pode ser implementada com segurança como Singleton .

Péter Török
fonte
1
Mesmo assim, um solteirão pode ser uma dor. Se você perceber que seu logger precisa de algum parâmetro adicional para alguns casos, você pode criar um novo método (e deixar o logger ficar mais feio) ou quebrar todos os consumidores do logger, mesmo que eles não liguem.
kyoryu
4
@kyoryu, estou falando sobre o caso "usual", que IMHO implica no uso de uma estrutura de registro padrão (de facto). (Que normalmente são configuráveis ​​por meio de arquivos de propriedade / XML aliás.) É claro que há exceções - como sempre. Se eu souber que meu aplicativo é excepcional nesse aspecto, não usarei um Singleton. Mas a superengenharia dizendo "isso pode ser útil em algum momento" é quase sempre um esforço perdido.
Péter Török
Se você já está usando DI, não é muita engenharia extra. (BTW, eu não discordo de você, eu sou o upvote). Muitos registradores exigem algumas informações de "categoria" ou algo do tipo, e adicionar parâmetros adicionais pode causar dor. Escondê-lo atrás de uma interface pode ajudar a manter o código de consumo limpo e pode facilitar a mudança para uma estrutura de registro diferente.
kyoryu
1
@kyoryu, desculpe pela suposição errada. Eu vi um voto negativo e um comentário, então conectei os pontos - da maneira errada, como se constatou :-( Nunca experimentei a necessidade de mudar para uma estrutura de registro diferente, mas, novamente, eu entendo que pode ser um legítimo preocupação em alguns projetos.
Péter Török
5
Corrija-me se eu estiver errado, mas o singleton seria para o LogFactory, não o logger. Além disso, o LogFactory provavelmente será o apache commons ou a fachada de registro do Slf4j. Portanto, mudar as implementações de registro é indolor de qualquer maneira. A verdadeira dor de usar DI para injetar uma LogFactory não é o fato de que você precisa ir para o applicationContext agora para criar todas as instâncias em seu aplicativo?
HDave de
9

É principalmente, mas não totalmente, sobre testes. Os solteiros eram populares porque eram fáceis de consumir, mas há uma série de desvantagens para os solteiros.

  • Difícil de testar. Significa como posso ter certeza de que o logger faz a coisa certa.
  • Difícil de testar. Ou seja, se estou testando o código que usa o logger, mas não é o foco do meu teste, ainda preciso garantir que meu env de teste suporte o logger
  • Às vezes você não quer um único, mas mais flexibilidade

O DI fornece o consumo fácil de suas classes dependentes - basta colocá-lo nos args do construtor e o sistema fornece isso para você - enquanto fornece a flexibilidade de teste e construção.

Scott Weinstein
fonte
Na verdade não. É sobre estado mutável compartilhado e dependências estáticas que tornam as coisas difíceis no longo prazo. O teste é apenas o exemplo óbvio e freqüentemente o mais doloroso.
kyoryu
2

Praticamente a única vez em que você deve usar um Singleton em vez de Dependency Injection é se o Singleton representa um valor imutável, como List.Empty ou semelhante (assumindo listas imutáveis).

A verificação do intestino para um Singleton deve ser "eu estaria bem se esta fosse uma variável global em vez de um Singleton?" Caso contrário, você está usando o padrão Singleton para encobrir uma variável global e deve considerar uma abordagem diferente.

Kyoryu
fonte
1

Acabei de verificar o artigo Monostate - é uma alternativa bacana ao Singleton, mas tem algumas propriedades estranhas:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

Isso não é assustador - porque o mapeador é realmente dependente da conexão do banco de dados para executar save () - mas se outro mapeador foi criado antes - ele pode pular esta etapa na aquisição de suas dependências. Embora seja legal, também é meio bagunçado, não é?

Sunwukung
fonte