Se Singletons são ruins, por que um Container de Serviço é bom?

91

Todos nós sabemos como os solteiros são ruins porque ocultam dependências e por outros motivos .

Mas em um framework, pode haver muitos objetos que precisam ser instanciados apenas uma vez e chamados de qualquer lugar (logger, db etc.).

Para resolver este problema, fui instruído a usar o chamado "Gerenciador de Objetos" (ou Contêiner de Serviço como o symfony) que armazena internamente todas as referências a Serviços (logger etc).

Mas por que um Provedor de Serviços não é tão ruim quanto um Singleton puro?

O provedor de serviços também oculta as dependências e apenas conclui a criação da primeira instância. Portanto, estou realmente lutando para entender por que devemos usar um provedor de serviços em vez de singletons.

PS. Eu sei que para não esconder dependências devo usar DI (conforme afirmado por Misko)

Adicionar

Eu acrescentaria: hoje em dia, os singletons não são tão maus, o criador do PHPUnit explicou aqui:

DI + Singleton resolve o problema:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

isso é muito inteligente, mesmo que não resolva todos os problemas.

Além de DI e Service Container, há alguma solução aceitável para acessar esses objetos auxiliares?

dinâmico
fonte
2
@yes Sua edição está fazendo suposições falsas. Sebastian não sugere, de forma alguma, que o trecho de código está tornando o uso de Singleons menos problemático. É apenas uma maneira de tornar o código que de outra forma seria impossível de testar mais testável. Mas ainda é um código problemático. Na verdade, ele observa explicitamente: "Só porque você pode, não significa que você deveria". A solução correta ainda seria não usar Singletons.
Gordon de
3
@sim segue o princípio SOLID.
Gordon
19
Eu discuto a afirmação de que solteiros são ruins. Eles podem ser mal utilizados, sim, mas qualquer ferramenta também pode . Um bisturi pode ser usado para salvar uma vida ou acabar com ela. Uma motosserra pode derrubar florestas para evitar incêndios florestais ou pode cortar uma parte considerável de seu braço se você não souber o que está fazendo. Aprenda a usar suas ferramentas com sabedoria e não trate os conselhos como uma verdade - assim está a mente impensada.
paxdiablo
4
@paxdiablo mas eles são ruins. Singletons violam SRP, OCP e DIP. Eles introduzem o estado global e o acoplamento estreito em seu aplicativo e farão com que sua API minta sobre suas dependências. Tudo isso afetará negativamente a capacidade de manutenção, legibilidade e testabilidade de seu código. Pode haver casos raros em que essas desvantagens superam os pequenos benefícios, mas eu diria que em 99% você não precisa de um Singleton. Especialmente em PHP, onde Singletons são únicos apenas para o Request e é muito simples montar gráficos de colaboradores a partir de um Builder.
Gordon
5
Não, eu não acredito nisso. Uma ferramenta é um meio de realizar uma função, geralmente tornando-a mais fácil de alguma forma, embora alguns (emacs?) Tenham a rara distinção de torná-la mais difícil :-) Neste, um singleton não é diferente de uma árvore balanceada ou de um compilador . Se você precisar garantir apenas uma cópia de um objeto, um singleton fará isso. Pode-se debater se funciona bem, mas não acredito que você possa argumentar que não funciona. E pode haver maneiras melhores, como uma motosserra sendo mais rápida do que uma serra manual ou uma pistola de pregos em vez de um martelo. Isso não torna o serrote / martelo menos uma ferramenta.
paxdiablo de

Respostas:

76

O Service Locator é apenas o menor de dois males, por assim dizer. O "menor" se resumindo a essas quatro diferenças ( pelo menos não consigo pensar em nenhuma outra agora ):

Princípio de Responsabilidade Única

O Service Container não viola o Princípio de Responsabilidade Única, como o Singleton faz. Singletons combinam criação de objetos e lógica de negócios, enquanto o Service Container é estritamente responsável por gerenciar os ciclos de vida de objetos de seu aplicativo. Nesse sentido, o Service Container é melhor.

Acoplamento

Os singletons geralmente são codificados permanentemente em seu aplicativo devido às chamadas de método estático, o que leva a dependências fortemente acopladas e difíceis de simular em seu código. O SL, por outro lado, é apenas uma classe e pode ser injetado. Portanto, embora todas as suas classes dependam disso, pelo menos é uma dependência fracamente acoplada. Portanto, a menos que você implemente o ServiceLocator como um Singleton, isso é um pouco melhor e também mais fácil de testar.

No entanto, todas as classes que usam o ServiceLocator agora dependerão do ServiceLocator, que também é uma forma de acoplamento. Isso pode ser atenuado usando uma interface para o ServiceLocator, de forma que você não esteja vinculado a uma implementação ServiceLocator concreta, mas suas classes dependerão da existência de algum tipo de Locator, ao passo que não usar um ServiceLocator aumenta drasticamente a reutilização.

Dependências ocultas

Porém, o problema de ocultar dependências existe muito. Quando você apenas injeta o localizador em suas classes de consumo, você não conhecerá nenhuma dependência. Mas, em contraste com o Singleton, o SL geralmente instancia todas as dependências necessárias nos bastidores. Então, quando você busca um serviço, você não termina como Misko Hevery no exemplo do CreditCard , por exemplo , você não tem que instanciar todas as dependências das dependências manualmente.

Buscar as dependências de dentro da instância também está violando a Lei de Demeter , que afirma que você não deve pesquisar colaboradores. Uma instância deve falar apenas com seus colaboradores imediatos. Este é um problema com Singleton e ServiceLocator.

Estado Global

O problema do estado global também é um pouco mitigado porque quando você instancia um novo Service Locator entre os testes, todas as instâncias criadas anteriormente são excluídas também (a menos que você cometeu o erro e as salvou em atributos estáticos no SL). Isso não vale para nenhum estado global em classes administradas pelo SL, é claro.

Consulte também Fowler em Service Locator vs Dependency Injection para uma discussão muito mais detalhada.


Uma observação sobre sua atualização e o artigo vinculado por Sebastian Bergmann sobre o código de teste que usa Singletons : Sebastian não sugere, de forma alguma, que a solução alternativa proposta torna o uso de Singleons menos problemático. É apenas uma maneira de tornar o código que de outra forma seria impossível de testar mais testável. Mas ainda é um código problemático. Na verdade, ele observa explicitamente: "Só porque você pode, não significa que você deveria".

Gordon
fonte
1
Especialmente a testabilidade deve ser reforçada aqui. Você não pode simular chamadas de método estático. No entanto, você pode simular serviços que foram injetados por meio do construtor ou setter.
David de
44

O padrão do localizador de serviço é um antipadrão. Isso não resolve o problema de expor dependências (você não pode dizer olhando para a definição de uma classe quais são suas dependências porque elas não estão sendo injetadas, em vez disso, estão sendo arrancadas do localizador de serviço).

Portanto, sua pergunta é: por que os localizadores de serviço são bons? Minha resposta é: eles não são.

Evite, evite, evite.

Jason
fonte
6
Parece que você não sabe nada sobre interfaces. A classe apenas descreve a interface necessária na assinatura do construtor - e é tudo o que ele precisa saber. O Passed Service Locator deve implementar a interface, só isso. E se o IDE verificar a implementação da interface, será muito fácil controlar quaisquer alterações.
OZ_
4
@ yes123: Pessoas que dizem isso estão erradas, e estão erradas porque o SL é um antipadrão. Sua pergunta é "por que o SL é bom?" Minha resposta é: eles não são.
jason
5
Não vou discutir se o SL é um anti-padrão ou não, mas o que direi é que é muito menos um dos males quando comparado ao singleton e aos globais. Você não pode testar uma classe que dependa de um singleton, mas definitivamente pode testar uma classe que dependa de um SL (embora você possa bagunçar o design do SL a ponto de não funcionar) ... Então, vale a pena notando ...
ircmaxell
3
@Jason você precisa passar o objeto que implementa a Interface - e é apenas o que você precisa saber. Você está se limitando apenas à definição do construtor de classe e deseja escrever no construtor todas as classes (não interfaces) - é uma ideia estúpida. Tudo que você precisa é interface. Você pode testar com sucesso esta classe com simulações, você pode facilmente alterar o comportamento sem alterar o código, não há dependências extras e acoplamento - isso é tudo (em geral) o que queremos ter na injeção de dependência.
OZ_
2
Claro, vou apenas juntar Banco de Dados, Agente de Log, Disco, Modelo, Cache e Usuário em um único objeto de "Entrada", com certeza será mais fácil dizer em quais dependências meu objeto depende do que se eu tivesse usado um contêiner.
Mahn
4

O container de serviço oculta dependências como o padrão Singleton faz. Você pode sugerir o uso de contêineres de injeção de dependência, já que ele tem todas as vantagens do contêiner de serviço, mas nenhuma desvantagem (pelo que eu sei) desse contêiner de serviço.

Pelo que entendi, a única diferença entre os dois é que no container de serviço, o container de serviço é o objeto que está sendo injetado (ocultando assim as dependências), quando você usa DIC, o DIC injeta as dependências apropriadas para você. A classe sendo gerenciada pelo DIC é completamente alheia ao fato de que é gerenciada por um DIC, portanto, você tem menos acoplamento, dependências claras e testes de unidade felizes.

Esta é uma boa pergunta no SO, explicando a diferença de ambos: Qual é a diferença entre os padrões de injeção de dependência e localizador de serviço?

Rickchristie
fonte
"o DIC injeta as dependências apropriadas para você" Isso não acontece com Singleton também?
dinâmico
5
@ yes123 - Se você estiver usando um Singleton, não o injetará, na maioria das vezes apenas o acessará globalmente (esse é o objetivo do Singleton). Suponho que se você disser que se injetar o Singleton, isso não ocultará as dependências, mas meio que anula o propósito original do padrão Singleton - você se perguntaria, se eu não preciso que essa classe seja acessada globalmente, por que eu preciso torná-lo Singleton?
rickchristie
2

Porque você pode facilmente substituir objetos no Service Container por
1) herança (a classe Object Manager pode ser herdada e os métodos podem ser substituídos)
2) alteração da configuração (no caso do Symfony)

E, Singletons são ruins não apenas por causa do alto acoplamento, mas porque são _ Single _tons. É uma arquitetura errada para quase todos os tipos de objetos.

Com DI 'puro' (em construtores) você pagará um preço muito alto - todos os objetos devem ser criados antes de serem passados ​​no construtor. Isso significará mais memória usada e menos desempenho. Além disso, nem sempre o objeto pode ser apenas criado e passado no construtor - uma cadeia de dependências pode ser criada ... Meu inglês não é bom o suficiente para discutir sobre isso completamente, leia sobre isso na documentação do Symfony.

OZ_
fonte
0

Para mim, tento evitar constantes globais, singletons por um motivo simples, há casos em que posso precisar de APIs em execução.

Por exemplo, tenho front-end e admin. No admin, quero que eles possam fazer o login como um usuário. Considere o código dentro do admin.

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

Isso pode estabelecer uma nova conexão de banco de dados, novo logger etc. para a inicialização do frontend e verificar se o usuário realmente existe, é válido, etc. Também usaria cookies separados e serviços de localização adequados.

Minha ideia de singleton é - Você não pode adicionar o mesmo objeto dentro do pai duas vezes. Por exemplo

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

deixaria você com uma única instância e ambas as variáveis ​​apontando para ela.

Finalmente, se você deseja usar o desenvolvimento orientado a objetos, trabalhe com objetos, não com classes.

Romaninsh
fonte
1
então o seu método é passar a $api var pelo seu framework? Eu não entendi exatamente o que você quis dizer. Além disso, se a chamada add('Logger')retornar a mesma instância, basicamente, você tem um co
dinâmico
sim, está certo. Eu me refiro a eles como "Controlador do sistema" e têm como objetivo aprimorar a funcionalidade da API. De forma semelhante, adicionar o controlador "Auditable" a um modelo duas vezes funcionaria exatamente da mesma maneira - criar apenas uma instância e apenas um conjunto de campos de auditoria.
romaninsh