Considere estes dois exemplos:
Passando um objeto para um construtor
class ExampleA
{
private $config;
public function __construct($config)
{
$this->config = $config;
}
}
$config = new Config;
$exampleA = new ExampleA($config);
Instanciando uma classe
class ExampleB
{
private $config;
public function __construct()
{
$this->config = new Config;
}
}
$exampleA = new ExampleA();
Qual é a maneira correta de lidar com a adição de um objeto como uma propriedade? Quando devo usar um sobre o outro? O teste de unidade afeta o que devo usar?
object-oriented
unit-testing
configuration
Prisioneiro
fonte
fonte
Respostas:
Eu acho que o primeiro lhe dará a capacidade de criar um
config
objeto em outro lugar e passá-lo paraExampleA
. Se você precisar de Injeção de Dependência, isso pode ser uma boa coisa, pois você pode garantir que todas as intenções compartilhem o mesmo objeto.Por outro lado, talvez você
ExampleA
exija umconfig
objeto novo e limpo, para que possa haver casos em que o segundo exemplo seja mais apropriado, como casos em que cada instância poderia ter uma configuração diferente.fonte
Não se esqueça da testabilidade!
Geralmente, se o comportamento da
Example
classe depende da configuração, você deseja testá-la sem salvar / modificar o estado interno da instância da classe (ou seja, você deseja ter testes simples para o caminho feliz e para a configuração incorreta sem modificar a propriedade / membro daExample
classe).Portanto, eu iria com a primeira opção de
ExampleA
fonte
Se o objeto tiver a responsabilidade de gerenciar o tempo de vida da dependência, não há problema em criar o objeto no construtor (e descartá-lo no destruidor). *
Se o objeto não for responsável por gerenciar o tempo de vida da dependência, ele deve ser passado para o construtor e gerenciado de fora (por exemplo, por um contêiner de IoC).
Nesse caso, não acho que sua ClassA deva assumir a responsabilidade de criar $ config, a menos que também seja responsável por descartá-la ou se a configuração for exclusiva para cada instância da ClassA.
* Para auxiliar na testabilidade, o construtor pode fazer referência a uma classe / método de fábrica para construir a dependência em seu construtor, aumentando a coesão e a testabilidade.
fonte
Recentemente, tive essa mesma discussão com nossa equipe de arquitetura e há alguns motivos sutis para fazê-lo de uma maneira ou de outra. Isso se resume principalmente à Injeção de Dependência (como outros observaram) e se você realmente tem ou não controle sobre a criação do objeto que você é novo no construtor.
No seu exemplo, e se a sua classe Config:
a) possui alocação não trivial, como proveniente de um pool ou método de fábrica.
b) pode falhar na alocação. Passá-lo para o construtor evita perfeitamente esse problema.
c) é realmente uma subclasse de Config.
Passar o objeto para o construtor oferece mais flexibilidade.
fonte
A resposta abaixo está errada, mas vou mantê-la para que outras pessoas aprendam com ela (veja abaixo)
Em
ExampleA
, você pode usar a mesmaConfig
instância em várias classes. No entanto, se houver apenas umaConfig
instância em todo o aplicativo, considere aplicar o padrão SingletonConfig
para evitar ter várias instâncias deConfig
. E seConfig
for um Singleton, você pode fazer o seguinte:Por
ExampleB
outro lado, você sempre terá uma instância separada deConfig
para cada instância deExampleB
.Qual versão você deve aplicar realmente depende de como o aplicativo tratará instâncias de
Config
:ExampleX
deve ter uma instância separada deConfig
, vá comExampleB
;ExampleX
compartilhar uma (e apenas uma) instância deConfig
useExampleA with Config Singleton
;ExampleX
podem usar diferentes instâncias deConfig
, permaneça comExampleA
.Por que converter
Config
em um Singleton está errado:Devo admitir que só aprendi sobre o padrão Singleton ontem (lendo o livro Head First de padrões de design). Ingenuamente, eu o apliquei neste exemplo, mas, como muitos apontaram, de uma maneira são outras (algumas foram mais enigmáticas e apenas disseram "Você está fazendo errado!"), Essa não é uma boa ideia. Portanto, para impedir que outras pessoas cometam o mesmo erro que eu cometi, segue um resumo de por que o padrão Singleton pode ser prejudicial (com base nos comentários e no que descobri pesquisando no Google):
Se
ExampleA
recuperar sua própria referência àConfig
instância, as classes serão fortemente acopladas. Não haverá comoExampleA
usar uma versão diferente deConfig
(por exemplo, uma subclasse). Isso é horrível se você deseja testarExampleA
usando uma instância de mock-up,Config
pois não há como fornecê-laExampleA
.A premissa de que haverá uma, e apenas uma, instância de
Config
talvez vale agora , mas você nem sempre pode ter certeza de que o mesmo será válido no futuro . Se em algum momento posterior se verificar que várias instâncias deConfig
serão desejáveis, não há como conseguir isso sem reescrever o código.Mesmo que a única instância de
Config
talvez seja verdadeira por toda a eternidade, pode acontecer que você queira usar algumas subclasses deConfig
(embora ainda tenha apenas uma instância). Mas, como o código obtém diretamente a instância por meiogetInstance()
deConfig
, que é umstatic
método, não há como obter a subclasse. Novamente, o código deve ser reescrito.O fato de que os
ExampleA
usosConfig
serão ocultados, pelo menos ao visualizar a API deExampleA
. Isso pode ou não ser uma coisa ruim, mas pessoalmente sinto que isso parece uma desvantagem; por exemplo, ao manter, não há uma maneira simples de descobrir quais classes serão afetadas pelas alteraçõesConfig
sem examinar a implementação de todas as outras classes.Mesmo que o fato de
ExampleA
usar um SingletonConfig
não seja um problema em si, ele ainda pode se tornar um problema do ponto de vista de teste. Os objetos Singleton terão o estado que persistirá até o término do aplicativo. Isso pode ser um problema ao executar testes de unidade, pois você deseja que um teste seja isolado de outro (ou seja, que a execução de um teste não afete o resultado de outro). Para corrigir isso, o objeto Singleton deve ser destruído entre cada execução de teste (possivelmente tendo que reiniciar o aplicativo inteiro), o que pode levar muito tempo (sem mencionar tedioso e irritante).Dito isto, fico feliz por ter cometido esse erro aqui e não na implementação de um aplicativo real. Na verdade, eu estava pensando em reescrever meu código mais recente para usar o padrão Singleton para algumas das classes. Embora eu pudesse reverter facilmente as alterações (tudo é armazenado em um SVN, é claro), ainda assim eu teria perdido tempo fazendo isso.
fonte
ExampleA
eConfig
- o que não é uma coisa boa.A coisa mais simples a fazer é associar-se
ExampleA
aConfig
. Você deve fazer a coisa mais simples, a menos que haja uma razão convincente para fazer algo mais complexo.Uma razão para dissociar
ExampleA
eConfig
seria melhorar a testabilidade doExampleA
. O acoplamento direto degradará a capacidade de teste deExampleA
seConfig
tiver métodos lentos, complexos ou com rápida evolução. Para o teste, um método é lento se executar mais do que alguns microssegundos. Se todos os métodos deConfig
são simples e rápido, então eu iria tomar a abordagem simples e direta casalExampleA
paraConfig
.fonte
Seu primeiro exemplo é um exemplo do padrão de injeção de dependência. Uma classe com uma dependência externa recebe a dependência pelo construtor, setter etc.
Essa abordagem resulta em código fracamente acoplado. A maioria das pessoas pensa que o acoplamento flexível é uma coisa boa, porque você pode facilmente substituir a configuração nos casos em que uma instância específica de um objeto precisa ser configurada diferentemente das outras, você pode passar um objeto de configuração simulado para teste e, portanto, em.
A segunda abordagem está mais próxima do padrão do criador do GRASP. Nesse caso, o objeto cria suas próprias dependências. Isso resulta em código fortemente acoplado, isso pode limitar a flexibilidade da classe e dificultar o teste. Se você precisar que uma instância de uma classe tenha uma dependência diferente das outras, sua única opção é subclassificá-la.
Entretanto, pode ser o padrão apropriado nos casos em que a vida útil do objeto dependente é ditada pela vida útil do objeto dependente e em que o objeto dependente não é usado em nenhum lugar fora do objeto que depende dele. Normalmente, eu aconselho o DI a ser a posição padrão, mas você não precisa descartar completamente a outra abordagem, desde que esteja ciente de suas consequências.
fonte
Se sua classe não expuser as
$config
classes externas, eu a criaria dentro do construtor. Dessa forma, você está mantendo seu estado interno privado.Se os
$config
requisitos exigirem que seu próprio estado interno seja definido corretamente (por exemplo, precise de uma conexão com o banco de dados ou alguns campos internos sejam inicializados) antes do uso, faz sentido adiar a inicialização para algum código externo (possivelmente uma classe de fábrica) e injetá-lo o construtor. Ou, como outros já apontaram, se precisar ser compartilhado entre outros objetos.fonte
ExampleA é dissociado da classe concreta Config, que é boa , desde que o objeto seja recebido se não for do tipo Config, mas o tipo de uma superclasse abstrata de Config.
ExampleB está fortemente acoplado à classe concreta Config, que é ruim .
Instanciar um objeto cria um forte acoplamento entre classes. Isso deve ser feito em uma classe Factory.
fonte