Está usando “novo” no construtor sempre ruim?

37

Eu li que usar "new" em um construtor (para qualquer outro objeto que não seja o valor simples) é uma prática ruim, pois torna o teste de unidade impossível (já que esses colaboradores também precisam ser criados e não podem ser ridicularizados). Como não sou realmente experiente em testes unitários, estou tentando reunir algumas regras que aprenderei primeiro. Além disso, essa é uma regra geralmente válida, independentemente do idioma usado?

    
por Ezoela Vacca 01.02.2018 / 16:23
fonte

7 respostas

37

Sempre há exceções, e eu discordo do "sempre" no título, mas sim, essa diretriz é geralmente válida, e também se aplica fora do construtor.

O uso de novo em um construtor viola o D em SOLID (princípio de inversão de dependência). Isso dificulta o seu código de teste, porque o teste de unidade tem tudo a ver com isolamento; é difícil isolar a classe se ela tiver referências concretas.

Não se trata apenas de testes unitários. E se eu quiser apontar um repositório para dois bancos de dados diferentes ao mesmo tempo? A capacidade de passar no meu próprio contexto permite-me instanciar dois repositórios diferentes apontando para locais diferentes.

Não usar o novo no construtor torna seu código mais flexível. Isso também se aplica a idiomas que podem usar construções diferentes de new para inicialização de objetos.

No entanto, claramente, você precisa usar o bom senso. Há muitas ocasiões em que é bom usar new , ou onde seria melhor não usar, mas você não terá consequências negativas. Em algum ponto em algum lugar, new precisa ser chamado. Apenas tenha muito cuidado ao chamar new dentro de uma classe da qual muitas outras classes dependem.

Fazer algo como inicializar uma coleção particular vazia em seu construtor é bom, e injetar isso seria um absurdo.

Quanto mais referências uma classe tiver, mais cuidadoso você deve ser em não chamar new de dentro dela.

    
por 01.02.2018 / 16:31
fonte
50

Embora eu seja a favor de usar o construtor para simplesmente inicializar a nova instância em vez de criar vários outros objetos, os objetos auxiliares são bons, e você deve usar seu julgamento para saber se algo é um auxiliar interno ou não .

Se a classe representa uma coleção, ela pode ter uma matriz ou lista auxiliar interna ou hashset. Usaria new para criar esses ajudantes e isso seria considerado normal. A classe não oferece injeção para usar diferentes auxiliares internos e não tem motivo para isso. Nesse caso, você deseja testar os métodos públicos do objeto, que podem ir acumulando, removendo e substituindo elementos na coleção.

Em certo sentido, a construção de classe de uma linguagem de programação é um mecanismo para criar abstrações de nível superior, e criamos essas abstrações para preencher a lacuna entre o domínio do problema e as primitivas da linguagem de programação. No entanto, o mecanismo de classes é apenas uma ferramenta; varia de acordo com a linguagem de programação e, em algumas linguagens, algumas abstrações de domínio simplesmente requerem vários objetos no nível da linguagem de programação.

Em resumo, você tem que usar um pouco de julgamento se a abstração simplesmente requer um ou mais objetos internos / auxiliares, enquanto ainda está sendo visto pelo chamador como uma única abstração, ou se os outros objetos seriam melhor expostos ao chamador para criar o controle de dependências, o que seria sugerido, por exemplo, quando o chamador vê esses outros objetos usando a classe.

    
por 01.02.2018 / 17:26
fonte
27

Nem todos os colaboradores são interessantes o suficiente para testar unidades separadamente, você pode (indiretamente) testá-los através da classe de hospedagem / instanciação. Isso pode não se alinhar com a idéia de algumas pessoas de precisar testar cada classe, cada método público, etc., especialmente ao fazer o teste depois. Ao usar o TDD, você pode refatorar esse 'colaborador' para extrair uma classe em que já está totalmente sob teste no primeiro processo de teste.

    
por 01.02.2018 / 16:30
fonte
12

As I am not really experienced in unit testing, I am trying to gather some rules that I will learn first.

Tenha cuidado ao aprender "regras" para problemas que você nunca encontrou. Se você se deparar com alguma "regra" ou "melhor prática", sugiro encontrar um exemplo simples de brinquedo de onde essa regra é "suposta" para ser usada e tentar resolver o problema você mesmo , ignorando o que a "regra" diz.

Nesse caso, você pode tentar criar 2 ou 3 classes simples e alguns comportamentos que devem ser implementados. Implemente as classes da maneira que parecer natural e escreva um teste de unidade para cada comportamento. Faça uma lista de todos os problemas encontrados, por exemplo se você começou com as coisas trabalhando de uma maneira, então teve que voltar e mudar depois; se você ficou confuso sobre como as coisas deveriam se encaixar; se você ficou irritado ao escrever clichê; etc.

Em seguida, tente resolver o mesmo problema seguindo a regra. Mais uma vez, faça uma lista dos problemas encontrados. Compare as listas e pense em quais situações podem ser melhores ao seguir a regra e quais não podem.

Quanto à sua pergunta, eu costumo favorecer uma abordagem portos e adaptadores , onde fazemos uma distinção entre "lógica central" e "serviços" (isso é semelhante a distinguir entre funções puras e procedimentos eficazes).

A lógica do núcleo é toda sobre o cálculo de coisas "dentro" do aplicativo, com base no domínio do problema. Ele pode conter classes como User , Document , Order , Invoice , etc. Não há problema em ter classes principais chamando new para outras classes principais, já que elas são detalhes de implementação "internos". Por exemplo, criar um Order também pode criar um Invoice e um Document detalhando o que foi pedido. Não há necessidade de zombar destes durante os testes, porque estas são as coisas reais que queremos testar!

As portas e adaptadores são como a lógica do núcleo interage com o mundo exterior. É aqui que coisas como Database , ConfigFile , EmailSender , etc. vivem. Estas são as coisas que tornam o teste difícil, por isso é aconselhável criar estas fora da lógica principal, e passá-las conforme necessário (com injeção de dependência, ou como argumentos de método, etc.).

Dessa forma, a lógica central (que é a parte específica do aplicativo, onde a lógica de negócios importante vive e está sujeita a maior rotatividade) pode ser testada por conta própria, sem precisar se preocupar com bancos de dados, arquivos, e-mails etc. Podemos apenas passar alguns valores de exemplo e verificar se obtemos os valores de saída corretos.

As portas e os adaptadores podem ser testados separadamente, usando simulações para o banco de dados, sistema de arquivos, etc., sem ter que se preocupar com a lógica de negócios. Podemos apenas passar alguns valores de exemplo e ter certeza de que eles estão sendo armazenados / lidos / enviados / etc. apropriadamente.

    
por 01.02.2018 / 22:52
fonte
6

Permita-me responder a pergunta, reunindo o que considero ser os pontos-chave aqui. Vou citar alguns usuários por brevidade.

There are always exceptions, but yes, this rule is generally valid, and also applies outside of the constructor as well.

Using new in a constructor violates the D in SOLID (dependency inversion principal). It makes your code hard to test because unit testing is all about isolation; it is hard to isolate class if it has concrete references.

-TheCatWhisperer-

Sim, o uso de new em construtores geralmente leva a falhas de projeto (por exemplo, acoplamento justo), o que torna nosso projeto rígido. Difícil de testar sim, mas não impossível. A propriedade em jogo aqui é resiliência (tolerância a alterações) 1 .

No entanto, a citação acima nem sempre é verdadeira. Em alguns casos, pode haver classes que sejam rigidamente acopladas . David Arno comentou um casal.

There are of course exceptions where the class is an immutable value object, an implementation detail, etc. Where they are supposed to be tightly coupled.

-David Arno-

Exatamente. Algumas classes (por exemplo, classes internas) poderiam ser meros detalhes de implementação da classe principal. Estes devem ser testados juntamente com a classe principal e não são necessariamente substituíveis ou extensíveis.

Além disso, se o nosso método SOLID nos faz extrair essas classes , poderíamos estar violando outro bom princípio. A chamada Lei de Demeter . O que, por outro lado, acho que é realmente importante do ponto de vista do design.

Assim, a resposta provável, como de costume, é depende . Usar new dentro de construtores pode ser uma prática ruim. Mas não sistematicamente sempre.

Então, nos leva a avaliar se as classes são detalhes de implementação (a maioria dos casos não são) da classe principal. Se forem, deixe-os em paz. Se não estiverem, considere técnicas como Composition Root ou Injeção de Dependência por Recipientes IoC .

1: o objetivo principal do SOLID não é tornar nosso código mais testável. É tornar nosso código mais tolerante a mudanças. Mais flexível e, consequentemente, mais fácil de testar

Nota: David Arno, TheWhisperCat, espero que você não se importe, eu citei você.

    
por 01.02.2018 / 17:34
fonte
3

Como um exemplo simples, considere o seguinte pseudocódigo

class Foo {
  private:
     class Bar {}
     class Baz inherits Bar {}
     Bar myBar
  public:
     Foo(bool useBaz) { if (useBaz) myBar = new Baz else myBar = new Bar; }
}

Como o new é um detalhe de implementação puro de Foo , e os dois Foo::Bar e Foo::Baz são parte de Foo , quando o teste unitário de Foo não faz sentido zombar de partes de% código%. Você apenas zomba das partes fora Foo quando o teste de unidade Foo .

    
por 02.02.2018 / 11:08
fonte
-3

Sim, usar 'novo' nas classes raiz do seu aplicativo é um cheiro de código. Isso significa que você está bloqueando a classe para usar uma implementação específica e não poderá substituir outra. Sempre opte por injetar a dependência no construtor. Dessa forma, você não apenas poderá injetar dependências com facilidade durante o teste, mas tornará sua aplicação muito mais flexível, permitindo que você substitua rapidamente diferentes implementações, se necessário.

EDITAR: Para downvoters - aqui está um link para um livro de desenvolvimento de software sinalizando 'novo' como um possível cheiro de código: link

    
por 01.02.2018 / 16:32
fonte