¿Usar "nuevo" en el constructor siempre es malo?

37

He leído que usar "nuevo" en un constructor (para cualquier otro objeto que no sea de valor simple) es una mala práctica, ya que hace que las pruebas unitarias sean imposibles (ya que esos colaboradores también deben crearse y no se pueden burlar). Como no tengo mucha experiencia en pruebas unitarias, estoy tratando de reunir algunas reglas que aprenderé primero. Además, ¿es esta una regla que generalmente es válida, independientemente del idioma utilizado?

Ezoela Vacca
fuente
99
No hace que las pruebas sean imposibles. Sin embargo, hace que mantener y probar su código sea más difícil. Tener una lectura de nuevo es pegamento, por ejemplo.
David Arno
38
"siempre" siempre es incorrecto. :) Hay mejores prácticas, pero abundan las excepciones.
Paul
63
¿Qué lenguaje es este? newsignifica cosas diferentes en diferentes idiomas.
user2357112 apoya a Monica
13
Esta pregunta depende un poco del idioma, ¿no? No todos los idiomas tienen incluso una newpalabra clave. ¿Sobre qué idiomas preguntas?
Bryan Oakley
77
Regla tonta. Si no usa la inyección de dependencia cuando debería, tiene muy poco que ver con la palabra clave "nueva". En lugar de decir que "usar nuevo es un problema si lo usa para romper la inversión de dependencia", simplemente diga "no rompa la inversión de dependencia".
Matt Timmermans

Respuestas:

36

Siempre hay excepciones, y discrepo con el 'siempre' en el título, pero sí, esta guía es generalmente válida y también se aplica fuera del constructor.

El uso de new en un constructor viola la D en SOLID (principio de inversión de dependencia). Hace que su código sea difícil de probar porque las pruebas unitarias tienen que ver con el aislamiento; Es difícil aislar la clase si tiene referencias concretas.

Sin embargo, no se trata solo de pruebas unitarias. ¿Qué sucede si quiero apuntar un repositorio a dos bases de datos diferentes a la vez? La capacidad de pasar en mi propio contexto me permite instanciar dos repositorios diferentes que apuntan a diferentes ubicaciones.

No usar new en el constructor hace que su código sea más flexible. Esto también se aplica a los lenguajes que pueden usar construcciones que no sean newpara la inicialización de objetos.

Sin embargo, claramente, debe usar su buen juicio. Hay muchas ocasiones en que está bien usarlo new, o donde sería mejor no hacerlo, pero no tendrá consecuencias negativas. En algún momento en algún lugar, newtiene que ser llamado. Solo tenga mucho cuidado al llamar newdentro de una clase de la que dependen muchas otras clases.

Hacer algo como inicializar una colección privada vacía en su constructor está bien, e inyectarlo sería absurdo.

Cuantas más referencias tenga una clase, más cuidadoso deberías ser para no llamar newdesde dentro.

TheCatWhisperer
fuente
12
Realmente no veo ningún valor en esta regla. Existen reglas y pautas sobre cómo evitar un acoplamiento estrecho en el código. Esta regla parece tratar exactamente el mismo problema, excepto que es increíblemente demasiado restrictiva sin darnos ningún valor adicional. ¿Entonces cuál es el punto? ¿Por qué crear un objeto de encabezado ficticio para mi implementación de LinkedList en lugar de tratar con todos esos casos especiales que surgen de head = nullalguna manera mejora el código? ¿Por qué sería mejor dejar las colecciones incluidas como nulas y crearlas a pedido que hacerlo en el constructor?
Voo
22
Te estás perdiendo el punto. Sí, un módulo de persistencia es absolutamente algo de lo que no debe depender un módulo de nivel superior. Pero "nunca usar nuevo" no se desprende de "algunas cosas deben inyectarse y no acoplarse directamente". Si toma su copia confiable de Agile Software Development, verá que Martin generalmente está hablando de módulos, no de clases individuales: "Los módulos de alto nivel se ocupan de las políticas de alto nivel de la aplicación. Estas políticas generalmente se preocupan poco por los detalles que implementan ellos."
Voo
11
Entonces, el punto es que debe evitar las dependencias entre módulos, particularmente entre módulos de diferentes niveles de abstracción. Pero un módulo puede ser más que una sola clase y simplemente no tiene sentido construir interfaces entre código estrechamente acoplado de la misma capa de abstracción. En una aplicación bien diseñada que sigue principios SÓLIDOS, no todas las clases deben implementar una interfaz y no todos los constructores siempre deben tomar las interfaces como parámetros.
Voo
18
Voté en contra porque es mucha sopa de moda y no mucha discusión sobre consideraciones prácticas. Hay algo sobre un repositorio para dos bases de datos, pero sinceramente, ese no es un ejemplo del mundo real.
jpmc26
55
@ jpmc26, BJoBnh Sin entrar en si apruebo esta respuesta o no, el punto se expresa muy claramente. Si cree que "principal de inversión de dependencia", "referencia concreta" o "inicialización de objeto" son solo palabras de moda, entonces realmente no sabe lo que significan. Eso no es culpa de la respuesta.
R. Schmitz
50

Si bien estoy a favor de usar el constructor para simplemente inicializar la nueva instancia en lugar de crear varios otros objetos, los objetos auxiliares están bien, y debe usar su juicio sobre si algo es un auxiliar interno o no.

Si la clase representa una colección, puede tener una matriz auxiliar interna o una lista o hashset. Se usaría newpara crear estos ayudantes y se consideraría bastante normal. La clase no ofrece inyección para usar diferentes ayudantes internos y no tiene ninguna razón para hacerlo. En este caso, desea probar los métodos públicos del objeto, que podrían ir a acumular, eliminar y reemplazar elementos en la colección.


En cierto sentido, la construcción de clase de un lenguaje de programación es un mecanismo para crear abstracciones de nivel superior, y creamos tales abstracciones para cerrar la brecha entre el dominio del problema y las primitivas del lenguaje de programación. Sin embargo, el mecanismo de clase es solo una herramienta; varía según el lenguaje de programación y, algunas abstracciones de dominio, en algunos lenguajes, simplemente requieren múltiples objetos al nivel del lenguaje de programación.

En resumen, debe juzgar si la abstracción simplemente requiere uno o más objetos internos / auxiliares, mientras la persona que llama todavía la ve como una sola abstracción o si los otros objetos estarían mejor expuestos a la persona que llama para crear control de dependencias, lo que se sugiere, por ejemplo, cuando la persona que llama ve estos otros objetos al usar la clase

Erik Eidt
fuente
44
+1. Esto es exactamente correcto. Debe identificar cuál es el comportamiento previsto de la clase, y eso determina qué detalles de implementación deben exponerse y cuáles no. Hay una especie de juego de intuición sutil que tienes que jugar para determinar cuál es la "responsabilidad" de la clase también.
jpmc26
27

No todos los colaboradores son lo suficientemente interesantes como para realizar pruebas unitarias por separado, podría (indirectamente) probarlos a través de la clase de alojamiento / creación de instancias. Es posible que esto no se alinee con la idea de algunas personas de necesitar evaluar cada clase, cada método público, etc. especialmente cuando se realiza la prueba después. Cuando use TDD, puede refactorizar este 'colaborador' extrayendo una clase donde ya está completamente bajo prueba desde su primer proceso de prueba.

Joppe
fuente
14
Not all collaborators are interesting enough to unit-test separatelyFin de la historia :-), este caso es posible y nadie se atrevió a mencionarlo. @Joppe, te animo a elaborar un poco la respuesta. Por ejemplo, podría agregar algunos ejemplos de clases que son simples detalles de implementación (no aptos para reemplazo) y cómo se pueden extraer más adelante si lo consideramos necesario.
Laiv
Los modelos de dominio @Laiv son típicamente concretos, no abstractos, no se trata de inyectar objetos anidados allí. Objeto de valor simple / objeto complejo que de otro modo no tiene lógica también son candidatos.
Joppe
44
+1, absolutamente. Si se configura un idioma para que tenga que llamar new File()para hacer cualquier cosa relacionada con el archivo, no tiene sentido prohibir esa llamada. ¿Qué vas a hacer, escribir pruebas de regresión contra el Filemódulo stdlib ? No es probable. Por otro lado, recurrir newa una clase inventada por uno mismo es más dudoso.
Kilian Foth
77
@KilianFoth: Sin embargo, la unidad de buena suerte prueba cualquier cosa que llame directamente new File().
Phoshi
1
Eso es una conjetura . Podemos encontrar casos en los que no tiene sentido, casos en los que no es útil y casos en los que tiene sentido y es útil. Es una cuestión de necesidades y preferencias.
Laiv 01 de
13

Como no tengo mucha experiencia en pruebas unitarias, estoy tratando de reunir algunas reglas que aprenderé primero.

Tenga cuidado al aprender "reglas" para problemas que nunca ha encontrado. Si se encuentra con alguna "regla" o "práctica recomendada", le sugiero que busque un ejemplo simple de juguete de dónde se supone que se debe usar esta regla, e intente resolver ese problema usted mismo , ignorando lo que dice la "regla".

En este caso, podría intentar crear 2 o 3 clases simples y algunos comportamientos que deberían implementar. Implemente las clases de la manera que se sienta natural y escriba una prueba unitaria para cada comportamiento. Haga una lista de cualquier problema que haya encontrado, por ejemplo, si comenzó con cosas que funcionan de una manera, luego tuvo que regresar y cambiarlo más tarde; si te confundiste sobre cómo se supone que las cosas encajan entre sí; si te molesta escribir boilerplate; etc.

Luego intente resolver el mismo problema siguiendo la "regla". Nuevamente, haga una lista de los problemas que encontró. Compare las listas y piense qué situaciones podrían ser mejores al seguir la regla y cuáles no.


En cuanto a su pregunta real, tiendo a favorecer un enfoque de puertos y adaptadores , donde hacemos una distinción entre "lógica central" y "servicios" (esto es similar a distinguir entre funciones puras y procedimientos efectivos).

La lógica central se trata de calcular cosas "dentro" de la aplicación, en función del dominio del problema. Podría contener clases como User, Document, Order, Invoice, etc bien de que tenga clases básicas llaman newpara otras clases principales, ya que son detalles de implementación "internos". Por ejemplo, crear un Orderpoder también crea un Invoicey un Documentdetalle de lo que se ordenó. No hay necesidad de burlarse de estos durante las pruebas, ¡porque estas son las cosas reales que queremos probar!

Los puertos y adaptadores son la forma en que la lógica central interactúa con el mundo exterior. Aquí es donde cosas como Database, ConfigFile, EmailSender, etc. viven. Estas son las cosas que dificultan las pruebas, por lo que es recomendable crearlas fuera de la lógica central y pasarlas según sea necesario (ya sea con inyección de dependencia o como argumentos de métodos, etc.).

De esta manera, la lógica central (que es la parte específica de la aplicación, donde vive la lógica comercial importante y está sujeta a la mayor rotación) se puede probar por sí sola, sin tener que preocuparse por las bases de datos, archivos, correos electrónicos, etc. Simplemente podemos pasar algunos valores de ejemplo y verificar que obtenemos los valores de salida correctos.

Los puertos y adaptadores se pueden probar por separado, utilizando simulacros para la base de datos, el sistema de archivos, etc. sin tener que preocuparse por la lógica empresarial. Simplemente podemos pasar algunos valores de ejemplo y asegurarnos de que estén almacenados / leídos / enviados / etc. apropiadamente.

Warbo
fuente
6

Permítame responder la pregunta, reuniendo lo que considero que son los puntos clave aquí. Citaré a algún usuario por brevedad.

Siempre hay excepciones, pero sí, esta regla es generalmente válida y también se aplica fuera del constructor.

El uso de new en un constructor viola la D en SOLID (principal de inversión de dependencia). Hace que su código sea difícil de probar porque las pruebas unitarias tienen que ver con el aislamiento; Es difícil aislar la clase si tiene referencias concretas.

-TheCatWhisperer-

Sí, el uso de newconstructores internos a menudo conduce a fallas de diseño (por ejemplo, acoplamiento apretado) que hace que nuestro diseño sea rígido. Difícil de probar sí, pero no imposible. La propiedad en juego aquí es la resistencia (tolerancia a los cambios) 1 .

Sin embargo, la cita anterior no siempre es cierta. En algunos casos, podría haber clases destinadas a estar estrechamente acopladas . David Arno ha comentado una pareja.

Por supuesto, hay excepciones en las que la clase es un objeto de valor inmutable , un detalle de implementación , etc., donde se supone que están estrechamente vinculadas .

-David Arno-

Exactamente. Algunas clases (por ejemplo, clases internas) podrían ser simples detalles de implementación de la clase principal. Estos deben ser probados junto con la clase principal, y no son necesariamente reemplazables o extensibles.

Además, si nuestro culto SÓLIDO nos hace extraer estas clases , podríamos estar violando otro buen principio. La llamada Ley de Deméter . Lo cual, por otro lado, considero que es realmente importante desde el punto de vista del diseño.

Entonces, la respuesta probable, como de costumbre, es depende . Usar newconstructores internos puede ser una mala práctica. Pero no siempre de manera sistemática.

Por lo tanto, nos lleva a evaluar si las clases son detalles de implementación (la mayoría de los casos no lo serán) de la clase principal. Si lo son, déjelos en paz. Si no lo son, considere técnicas como la raíz de composición o la inyección de dependencia por contenedores IoC .


1: el objetivo principal de SOLID no es hacer que nuestro código sea más verificable. Es para hacer que nuestro código sea más tolerante a los cambios. Más flexible y, en consecuencia, más fácil de probar.

Nota: David Arno, TheWhisperCat, espero que no te importe que te haya citado.

Laiv
fuente
3

Como un ejemplo simple, considere el siguiente 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; }
}

Dado que newes un simple detalle de implementación de Foo, y ambos Foo::Bary Foo::Bazson parte de Foo, cuando las pruebas unitarias de Foono tienen sentido burlarse de las partes Foo. Solo se burla de las partes exteriores Foo cuando realiza pruebas unitarias Foo.

MSalters
fuente
-3

Sí, usar 'nuevo' en las clases raíz de su aplicación es un olor a código. Significa que está bloqueando a la clase para que use una implementación específica, y no podrá sustituirla por otra. Opte siempre por inyectar la dependencia en el constructor. De esa manera, no solo podrá inyectar fácilmente dependencias simuladas durante las pruebas, sino que hará que su aplicación sea mucho más flexible al permitirle sustituir rápidamente diferentes implementaciones si es necesario.

EDITAR: Para los votantes que votan abajo: aquí hay un enlace a un libro de desarrollo de software que marca 'nuevo' como posible olor a 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

Eterno21
fuente
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 qué es esto un problema? No todas las dependencias de un árbol de dependencias deberían poder reemplazarse
Laiv
44
@Paul: Tener una implementación predeterminada significa que debe tomar una referencia estrechamente vinculada a la clase concreta especificada como predeterminada. Sin embargo, eso no lo convierte en el llamado "olor a código".
Robert Harvey
8
@EzoelaVacca: Sería cauteloso al usar las palabras "olor a código" en cualquier contexto. Es como decir "republicano" o "demócrata"; ¿Qué significan esas palabras? Tan pronto como le das a algo una etiqueta como esa, dejas de pensar en los problemas reales y el aprendizaje se detiene.
Robert Harvey
44
"Más flexible" no es automáticamente "mejor".
cuál es
44
@EzoelaVacca: Usar la newpalabra clave no es una mala práctica, y nunca lo fue. Lo importante es cómo usas la herramienta. No utiliza un mazo, por ejemplo, donde un martillo de bola será suficiente.
Robert Harvey