O uso de “novo” no construtor sempre é ruim?

37

Eu li que usar "novo" em um construtor (para outros objetos que não sejam de valor simples) é uma prática ruim, pois torna impossível o teste de unidade (pois esses colaboradores também precisam ser criados e não podem ser ridicularizados). Como não tenho muita experiência em testes de unidade, estou tentando reunir algumas regras que aprenderei primeiro. Além disso, essa regra é geralmente válida, independentemente do idioma usado?

Ezoela Vacca
fonte
9
Não torna os testes impossíveis. No entanto, torna mais difícil manter e testar seu código. Ter uma leitura de novo é cola, por exemplo.
David Arno
38
"sempre" está sempre incorreto. :) Existem práticas recomendadas, mas há muitas exceções.
Paulo
63
Que língua é essa? newsignifica coisas diferentes em diferentes idiomas.
User2357112 suporta Monica
13
Esta questão depende um pouco da linguagem, não é? Nem todos os idiomas têm uma newpalavra - chave. Sobre quais idiomas você está perguntando?
Bryan Oakley
7
Regra boba. Deixar de usar a injeção de dependência quando você deve ter muito pouco a ver com a palavra-chave "new". Em vez de dizer que "usar novo é um problema se você usá-lo para interromper a inversão de dependência", basta dizer "não interrompa a inversão de dependência".
214188 Timothymans

Respostas:

36

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 new em um construtor viola o D no SOLID (princípio de inversão de dependência). Isso torna seu código difícil de testar, 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 de unidade. 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 me permite instanciar dois repositórios diferentes apontando para locais diferentes.

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

No entanto, claramente, você precisa usar o bom senso. Há muitas vezes em que é bom usar newou onde seria melhor não usar , mas você não terá consequências negativas. Em algum momento em algum lugar, newtem que ser chamado. Apenas tenha muito cuidado ao chamar newdentro de uma classe da qual muitas outras classes dependem.

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

Quanto mais referências uma classe tiver, mais cuidado você deve ter para não ligar newde dentro dela.

TheCatWhisperer
fonte
12
Realmente não vejo valor nesta regra. Existem regras e diretrizes sobre como evitar o acoplamento rígido no código. Essa regra parece lidar exatamente com o mesmo problema, exceto que é incrivelmente restritiva demais, sem nos fornecer nenhum valor adicional. Então qual é o objetivo? Por que criar um objeto de cabeçalho fictício para minha implementação do LinkedList em vez de lidar com todos os casos especiais que resultam de head = nullalguma forma melhorar o código? Por que deixar coleções incluídas como nulas e criá-las sob demanda é melhor do que fazê-lo no construtor?
Voo
22
Você está perdendo o ponto. Sim, um módulo de persistência é absolutamente algo que um módulo de nível superior não deve depender. Mas "nunca use novo" não segue de "algumas coisas devem ser injetadas e não diretamente acopladas". Se você fizer sua cópia confiável do Agile Software Development, verá que Martin geralmente está falando de módulos e não de classes únicas: "Os módulos de alto nível lidam com as políticas de alto nível do aplicativo. Essas políticas geralmente se importam pouco com os detalhes que implementam eles."
Voo
11
Portanto, o ponto é que você deve evitar dependências entre módulos - particularmente entre módulos de diferentes níveis de abstração. Mas um módulo pode ser mais do que uma única classe e simplesmente não faz sentido erguer interfaces entre códigos fortemente acoplados da mesma camada de abstração. Em um aplicativo bem projetado que segue os princípios do SOLID, nem todas as classes devem implementar uma interface e nem todo construtor deve sempre considerar as interfaces apenas como parâmetros.
Voo
18
Eu diminuí a votação porque é muita sopa de chavão e não há muita discussão sobre considerações práticas. Há algo sobre um repo para dois DBs, mas honestamente, esse não é um exemplo do mundo real.
Jpmc26
5
@ jpmc26, BJoBnh Sem entrar em detalhes, se eu desculpo ou não essa resposta, o ponto é expresso com muita clareza. Se você acha que "dependência de inversão principal", "referência concreta" ou "inicialização de objeto" são apenas palavras de ordem, na verdade você simplesmente não sabe o que elas significam. Isso não é culpa da resposta.
R. Schmitz
50

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

Se a classe representa uma coleção, ela pode ter uma matriz auxiliar interna ou lista ou hashset. Seria usado newpara criar esses auxiliares e seria considerado bastante normal. A classe não oferece injeção para usar ajudantes internos diferentes e não tem motivos para isso. Nesse caso, você deseja testar os métodos públicos do objeto, que podem acumular, remover e substituir 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 classe é 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ê deve julgar se a abstração requer apenas um ou mais objetos internos / auxiliares, enquanto continua sendo vista pelo chamador como uma única abstração ou se os outros objetos estariam melhor expostos ao chamador para criar para controle de dependências, o que seria sugerido, por exemplo, quando o chamador vê esses outros objetos usando a classe

Erik Eidt
fonte
4
+1. Isto é exatamente correto. Você precisa identificar qual é o comportamento pretendido da classe e isso determina quais detalhes da implementação precisam ser expostos e quais não. Há um tipo de jogo de intuição sutil que você precisa para determinar qual é a "responsabilidade" da classe também.
jpmc26
27

Nem todos os colaboradores são interessantes o suficiente para fazer o teste unitário separadamente; você pode (indiretamente) testá-los através da classe de hospedagem / instanciação. Isso pode não estar alinhado 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', extraindo uma classe onde ela já está totalmente em teste desde o primeiro processo de teste.

Joppe
fonte
14
Not all collaborators are interesting enough to unit-test separatelyfim da história :-), este caso é possível e ninguém se atreveu a mencionar. @Joppe Convido você a elaborar a resposta um pouco. Por exemplo, você pode adicionar alguns exemplos de classes que são meros detalhes de implementação (não adequados para substituição) e como elas podem ser extraídas posteriormente, se considerarmos necessário.
LAIV
Os modelos de domínio @Laiv geralmente são concretos, não abstratos; você não injeta objetos aninhados lá. Objeto de valor simples / objeto complexo que, de outra forma, não teria lógica, também é candidato.
Joppe
4
+1, absolutamente. Se um idioma estiver configurado para que você precise ligar new File()para fazer algo relacionado a arquivos, não há sentido em proibir essa chamada. O que você vai fazer, escrever testes de regressão no Filemódulo stdlib ? Não é provável. Por outro lado, convocar newuma classe auto-inventada é mais duvidosa.
Kilian Foth
7
@KilianFoth: Unidade de boa sorte testando qualquer coisa que chama diretamente new File(), no entanto.
Phoshi #
11
Isso é uma adivinhação . Podemos encontrar casos em que não faz sentido, casos em que não é útil e casos em que faz sentido e é útil. É uma questão de necessidades e preferências.
LAIV
13

Como não tenho muita experiência em testes de unidade, estou tentando reunir algumas regras que aprenderei primeiro.

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 onde essa regra "deve" ser usada e tentar resolver esse problema sozinho , ignorando o que a "regra" diz.

Nesse caso, você pode tentar criar 2 ou 3 classes simples e alguns comportamentos que eles devem implementar. 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 que você encontrou, por exemplo, se você começou com as coisas funcionando de uma maneira, teve que voltar e mudar mais tarde; se você ficou confuso sobre como as coisas devem se encaixar; se você se aborreceu ao escrever clichê; etc.

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


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

A lógica principal é calcular as coisas "dentro" do aplicativo, com base no domínio do problema. Ela pode conter classes como User, Document, Order, Invoice, do etc. É bom ter classes principais chamar newpara outras classes principais, já que eles são detalhes "internas" de implementação. Por exemplo, criar um Ordertambém pode criar um Invoicee um Documentdetalhamento do que foi solicitado. Não é necessário zombar deles durante os testes, porque essas são as coisas reais que queremos testar!

As portas e os adaptadores são como a lógica principal interage com o mundo externo. Isto é onde as coisas como Database, ConfigFile, EmailSender, etc. viver. Essas são as coisas que dificultam o teste; portanto, é aconselhável criá-las fora da lógica principal e transmiti-las conforme necessário (com injeção de dependência ou como argumentos de método etc.).

Dessa forma, a lógica principal (que é a parte específica do aplicativo, onde a importante lógica de negócios vive e está sujeita à maior rotatividade) pode ser testada sozinha, sem a necessidade de se preocupar com bancos de dados, arquivos, emails, 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 precisar se preocupar com a lógica de negócios. Podemos apenas passar alguns valores de exemplo e garantir que eles estejam sendo armazenados / lidos / enviados / etc. adequadamente.

Warbo
fonte
6

Permita-me responder à pergunta, reunindo o que considero os pontos-chave aqui. Vou citar alguns usuários por questões de concisão.

Sempre há exceções, mas sim, essa regra geralmente é válida e também se aplica fora do construtor.

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

-TheCatWhisperer-

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

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

É claro que há exceções em que a classe é um objeto de valor imutável , um detalhe de implementação etc. Onde elas devem estar fortemente acopladas .

-David Arno-

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

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

Portanto, a resposta provável, como sempre, é depende . Usar newconstrutores internos pode ser uma má prática. Mas nem sempre sistematicamente.

Portanto, é necessário avaliar se as classes são detalhes de implementação (na maioria dos casos, não serão) da classe principal. Se estiverem, deixe-os em paz. Caso contrário, considere técnicas como Root de Composição ou Injeção de Dependência por Contêineres IoC .


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

Nota: David Arno, TheWhisperCat, espero que você não se importe de ter citado você.

Laiv
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; }
}

Uma vez que o newé um detalhe de implementação do pura Foo, e ambos Foo::Bare Foo::Bazsão parte de Foo, quando a unidade de teste de Foonão há nenhum ponto em zombando partes Foo. Você zomba apenas das peças externas Foo quando faz o teste de unidade Foo.

MSalters
fonte
-3

Sim, usar 'novo' nas classes raiz do 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 facilmente dependências simuladas durante o teste, como também tornará seu aplicativo muito mais flexível, permitindo que você substitua rapidamente diferentes implementações, se necessário.

EDIT: Para downvoters - aqui está um link para um livro de desenvolvimento de software sinalizando 'novo' como um possível cheiro de código: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = onepage & q = new% 20keyword% 20code% 20smell & f = false

Eternal21
fonte
15
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.Por que isso é um problema? Não cada dependência única em uma árvore de dependência deve ser aberto a substituição
LAIV
4
@Paul: Ter uma implementação padrão significa que você faz uma referência rigorosa à classe concreta especificada como padrão. Isso não faz com que seja o chamado "cheiro de código".
Robert Harvey
8
@EzoelaVacca: Eu seria cauteloso ao usar as palavras "cheiro de código" em qualquer contexto. É como dizer "republicano" ou "democrata"; o que essas palavras significam? Assim que você define um rótulo para algo assim, você para de pensar nos problemas reais e o aprendizado para.
Robert Harvey
4
"Mais flexível" não é automaticamente "melhor".
Whatsisname
4
@EzoelaVacca: Usar a newpalavra-chave não é uma prática ruim e nunca foi. É como você usa a ferramenta que importa. Você não usa uma marreta, por exemplo, onde um martelo de bola é suficiente.
Robert Harvey