¿Por qué es realmente malo un ciclo de tiempo (verdadero) en un constructor?

47

Aunque es una pregunta general, mi alcance es más bien C #, ya que soy consciente de que los lenguajes como C ++ tienen diferentes semánticas con respecto a la ejecución del constructor, la administración de memoria, el comportamiento indefinido, etc.

Alguien me hizo una pregunta interesante que no fue fácil de responder para mí.

¿Por qué (o es en absoluto?) considerado un mal diseño para permitir que un constructor de una clase inicie un ciclo sin fin (es decir, un ciclo de juego)?

Hay algunos conceptos que están rotos por esto:

  • al igual que el principio de menos asombro, el usuario no espera que el constructor se comporte de esta manera.
  • Las pruebas unitarias son más difíciles ya que no puedes crear esta clase o inyectarla ya que nunca sale del bucle.
  • El final del bucle (final del juego) es conceptualmente el momento en que el constructor termina, lo que también es impar.
  • Técnicamente, esta clase no tiene miembros públicos excepto el constructor, lo que hace que sea más difícil de entender (especialmente para los idiomas donde no hay implementación disponible)

Y luego están los problemas técnicos:

  • El constructor en realidad nunca termina, así que, ¿qué pasa con GC aquí? ¿Este objeto ya está en Gen 0?
  • Derivar de tal clase es imposible o, al menos, muy complicado debido al hecho de que el constructor base nunca devuelve

¿Hay algo más obvio que sea malo o tortuoso con tal enfoque?

    
pregunta Samuel 08.09.2016 - 11:04

9 respuestas

249

¿Cuál es el propósito de un constructor? Devuelve un objeto recién construido. ¿Qué hace un bucle infinito? Nunca vuelve. ¿Cómo puede el constructor devolver un objeto recién construido si no lo hace? No puede.

Ergo, un bucle infinito rompe el contrato fundamental de un constructor: construir algo.

    
respondido por el Jörg W Mittag 08.09.2016 - 12:59
44
  

¿Hay algo más obvio que sea malo o tortuoso con tal enfoque?

Sí, por supuesto. Es innecesario, inesperado, inútil, poco elegante. Viola los conceptos modernos de diseño de clase (cohesión, acoplamiento). Rompe el contrato de método (un constructor tiene un trabajo definido y no es solo un método aleatorio). Ciertamente no es fácil de mantener, los futuros programadores pasarán mucho tiempo tratando de entender lo que está pasando y tratando de adivinar las razones por las que se hizo de esa manera.

Nada de esto es un "error" en el sentido de que su código no funciona. Pero es probable que a la larga incurra en grandes costos secundarios (en relación con el costo de escribir el código inicialmente) haciendo que el código sea más difícil de mantener (es decir, difícil de agregar pruebas, difícil de reutilizar, difícil de depurar, difícil de extender, etc.) .).

Muchas / las más modernas mejoras en los métodos de desarrollo de software se realizan específicamente para facilitar el proceso real de escritura / prueba / depuración / mantenimiento del software. Todo esto es evitado por cosas como esta, donde el código se coloca al azar porque "funciona".

Lamentablemente, con frecuencia te encontrarás con programadores que ignoran todo esto. Funciona, eso es todo.

Para terminar con una analogía (otro lenguaje de programación, aquí el problema actual es calcular 2+2 ):

$sum_a = 'bash -c "echo $((2+2)) 2>/dev/null"';   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 2+2;

¿Qué está mal con el primer enfoque? Devuelve 4 después de un período de tiempo razonablemente corto; es correcto. Las objeciones (junto con todas las razones habituales que un desarrollador puede dar para refutarlas) son:

  • Más difícil de leer (pero hey, un buen programador puede leerlo tan fácilmente, y siempre lo hemos hecho así; si lo deseas, puedo refactorizarlo con un método)
  • Más lento (pero esto no está en un lugar crítico de tiempo, por lo que podemos ignorarlo y, además, ¡es mejor optimizar solo cuando sea necesario!)
  • Utiliza muchos más recursos (un nuevo proceso, más RAM, etc., pero dito, nuestro servidor es más que suficientemente rápido)
  • Introduce las dependencias en bash (pero nunca se ejecutará en Windows, Mac o Android)
  • Y todas las otras razones mencionadas anteriormente.
respondido por el AnoE 08.09.2016 - 18:12
8

Das suficientes razones en tu pregunta para descartar este enfoque, pero la pregunta real aquí es "¿Hay algo más obvio que sea malo o tortuoso con tal enfoque?"

Mi primera, aunque aquí fue que esto no tiene sentido. Si su constructor nunca termina, ninguna otra parte del programa puede obtener una referencia al objeto construido , de modo que, ¿cuál es la lógica detrás de ponerlo en un constructor en lugar de un método normal? Entonces se me ocurrió que la única diferencia sería que en su bucle podría permitir que las referencias a this parcialmente escapadas del bucle. Si bien esto no se limita estrictamente a esta situación, se garantiza que si permite que this se escape, esas referencias siempre apuntarán a un objeto que no está completamente construido.

No sé si la semántica en torno a este tipo de situación está bien definida en C #, pero diría que no importa porque no es algo en lo que la mayoría de los desarrolladores querrán profundizar.

    
respondido por el JimmyJames 08.09.2016 - 18:58
1

+1 Para menos asombro, pero este es un concepto difícil de articular para los nuevos desarrolladores. A un nivel pragmático, es difícil depurar las excepciones generadas dentro de los constructores, si el objeto no se inicializa, no existirá para que usted inspeccione el estado o el registro desde fuera de ese constructor.

Si siente la necesidad de hacer este tipo de patrón de código, use métodos estáticos en la clase.

Existe un constructor para proporcionar la lógica de inicialización cuando un objeto se crea una instancia de una definición de clase. Construye esta instancia de objeto porque es un contenedor que encapsula un conjunto de propiedades y funcionalidades a las que desea llamar desde el resto de la lógica de su aplicación.

Si no tiene la intención de usar el objeto que está "construyendo", ¿cuál es el punto de instanciar el objeto en primer lugar? Tener un bucle de tiempo (verdadero) en un constructor significa efectivamente que nunca pretendes que se complete ...

C # es un lenguaje muy rico orientado a objetos con muchas construcciones y paradigmas diferentes para que puedas explorar, conocer tus herramientas y cuándo usarlas, en la línea de fondo:

  

En C # no ejecute la lógica extendida o interminable dentro de los constructores porque ... hay mejores alternativas

    
respondido por el Chris Schaller 10.09.2016 - 16:37
0

No hay nada inherentemente malo en ello. Sin embargo, lo que será importante es la pregunta de por qué eligió usar este constructo. ¿Qué obtuviste al hacer esto?

Primero que nada, no voy a hablar sobre el uso de while(true) en un constructor. No hay absolutamente nada de malo en usar esa sintaxis en un constructor. Sin embargo, el concepto de "iniciar un ciclo de juego en el constructor" puede causar algunos problemas.

Es muy común en estas situaciones que el constructor simplemente construya el objeto y tenga una función que llame al bucle infinito. Esto le da al usuario de su código más flexibilidad. Como ejemplo de los tipos de problemas que pueden aparecer es que restringe el uso de su objeto. Si alguien quiere tener un objeto miembro que sea "el objeto del juego" en su clase, tiene que estar preparado para ejecutar todo el ciclo del juego en su constructor porque tiene que construir el objeto. Esto podría llevar a una codificación torturada para solucionar esto. En el caso general, no puede asignar previamente su objeto de juego y luego ejecutarlo más tarde. Para algunos programas eso no es un problema, pero hay clases de programas en los que deseas poder asignar todo por adelantado y luego llamar al bucle del juego.

Una de las reglas básicas en la programación es utilizar la herramienta más sencilla para el trabajo. No es una regla dura y rápida, pero es muy útil. Si una llamada de función hubiera sido suficiente, ¿por qué molestarse en construir un objeto de clase? Si veo que se usa una herramienta complicada más poderosa, asumo que el desarrollador tuvo una razón para ello, y comenzaré a explorar qué tipo de trucos extraños podría estar haciendo.

Hay casos en que esto podría ser razonable. Puede haber casos en los que sea verdaderamente intuitivo para usted tener un ciclo de juego en el constructor. En esos casos, corres con eso! Por ejemplo, su bucle de juego puede estar integrado en un programa mucho más grande que construye clases para incorporar algunos datos. Si tiene que ejecutar un bucle de juego para generar esos datos, puede ser muy razonable ejecutar el bucle de juego en un constructor. Simplemente trate esto como un caso especial: sería válido hacerlo porque el programa general hace que sea intuitivo para el próximo desarrollador entender lo que hizo y por qué.

    
respondido por el Cort Ammon 08.09.2016 - 21:21
0

Mi impresión es que algunos conceptos se confundieron y dieron como resultado una pregunta no tan bien formulada.

El constructor de una clase puede iniciar un nuevo hilo que "nunca" terminará (aunque prefiero usar while(_KeepRunning) sobre while(true) porque puedo establecer el miembro booleano en falso en algún lugar).

Podría extraer ese método para crear e iniciar el subproceso en una función propia, para separar la construcción del objeto y el inicio real de su trabajo. Prefiero esta manera porque tengo un mejor control sobre el acceso a los recursos.

También puede echar un vistazo al "Patrón de objeto activo". Al final, creo que la pregunta estaba dirigida a cuándo comenzar el hilo de un "Objeto activo", y mi preferencia es un desacoplamiento de la construcción y comenzar para un mejor control.

    
respondido por el Bernhard Hiller 09.09.2016 - 09:07
0

Su objeto me recuerda el inicio de Tasks . El objeto de tarea tiene un constructor, pero también hay un montón de métodos de fábrica estáticos como: Task.Run , Task.Start y Task.Factory.StartNew .

Estos son muy similares a lo que intentas hacer, y es probable que esta sea la "convención". El principal problema que la gente parece tener es que este uso de un constructor les sorprende. @ JörgWMittag dice que rompe un contrato fundamental , lo que supongo significa que Jörg está muy sorprendido. Estoy de acuerdo y no tengo nada más que agregar a eso.

Sin embargo, quiero sugerir que el OP intente métodos estáticos de fábrica. La sorpresa se desvanece y no hay contrato fundamental en juego aquí. La gente está acostumbrada a los métodos de fábrica estática que hacen cosas especiales, y pueden nombrarse en consecuencia.

Puede proporcionar constructores que permitan un control preciso (como sugiere @Brandin, algo como var g = new Game {...}; g.MainLoop(); , que se adapta a los usuarios que no quieren iniciar el juego de inmediato, y tal vez quieran pasarlo primero. Y puede escribe algo como var runningGame = Game.StartNew(); para que comenzar de inmediato sea fácil.

    
respondido por el Nathan Cooper 09.09.2016 - 14:54
0
  

¿Por qué un bucle while (verdadero) en un constructor es realmente malo?

Eso no es malo en absoluto.

  

¿Por qué (o es en absoluto?) considerado un mal diseño para permitir que un constructor de una clase inicie un ciclo sin fin (es decir, un ciclo de juego)?

¿Por qué querrías hacer esto? Seriamente. Esa clase tiene una sola función: el constructor. Si todo lo que quieres es una sola función, es por eso que tenemos funciones, no constructores.

Usted mismo mencionó algunas de las razones obvias en contra, pero omitió la más grande:

Hay opciones mejores y más simples para lograr lo mismo.

Si hay 2 opciones y una es mejor, no importa por cuánto sea mejor, eligió la mejor. Por cierto, es por eso que no casi nunca usamos GoTo.

    
respondido por el Peter 12.09.2016 - 13:05
-1

El propósito de un constructor es, bueno, construir su objeto. Antes de que el constructor haya completado, en principio no es seguro usar ningún método de ese objeto. Por supuesto, podemos llamar a los métodos del objeto dentro del constructor, pero cada vez que lo hacemos, debemos asegurarnos de que hacerlo sea válido para el objeto en su estado actual de medio horneado.

Al poner indirectamente toda la lógica en el constructor, agregas más carga de este tipo sobre ti. Esto sugiere que no diseñó la diferencia entre la inicialización y el uso lo suficientemente bien.

    
respondido por el Hagen von Eitzen 10.09.2016 - 11:22

Lea otras preguntas en las etiquetas