Aunque es una pregunta general, mi alcance es más bien C #, ya que soy consciente de que lenguajes como C ++ tienen una semántica diferente con respecto a la ejecución del constructor, la gestión de la memoria, el comportamiento indefinido, etc.
Alguien me hizo una pregunta interesante que para mí no fue respondida fácilmente.
¿Por qué (o se considera que es) un mal diseño permitir que un constructor de una clase inicie un ciclo sin fin (es decir, un ciclo de juego)?
Hay algunos conceptos que se rompen por esto:
- Al igual que el principio de menor asombro, el usuario no espera que el constructor se comporte así.
- Las pruebas unitarias son más difíciles ya que no puede crear esta clase o inyectarla, ya que nunca sale del bucle.
- El final del ciclo (final del juego) es, conceptualmente, el momento en que termina el constructor, que también es extraño.
- Técnicamente, tal clase no tiene miembros públicos, excepto el constructor, lo que hace que sea más difícil de entender (especialmente para los lenguajes donde no hay implementación disponible)
Y luego están los problemas técnicos:
- El constructor en realidad nunca termina, entonces, ¿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 regresa
¿Hay algo más obviamente malo o tortuoso con este enfoque?
c#
architecture
Samuel
fuente
fuente
var g = new Game {...}; g.MainLoop();
while(true)
bucle en un montador de propiedad:new Game().RunNow = true
?Respuestas:
¿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 devuelve nada? No puede
Ergo, un bucle infinito rompe el contrato fundamental de un constructor: construir algo.
fuente
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 del 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 comprender lo que está sucediendo 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 probablemente incurrirá en enormes costos secundarios (en relación con el costo de escribir el código inicialmente) a largo plazo al hacer que el código sea más difícil de mantener (es decir, pruebas difíciles de agregar, difíciles de reutilizar, difíciles de depurar, difíciles de extender, etc. .).
Muchas de las mejoras más modernas 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 burlado por cosas como esta, donde el código se coloca al azar porque "funciona".
Desafortunadamente, regularmente se encontrará con programadores que ignoran por completo todo esto. Funciona, eso es todo.
Para finalizar con una analogía (otro lenguaje de programación, aquí el problema en cuestión es calcular
2+2
):¿Qué hay de malo 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:
bash
(pero de todos modos nunca se ejecutará en Windows, Mac o Android)fuente
fundamental contract of a constructor: to construct something
.Usted da suficientes razones en su pregunta para descartar este enfoque, pero la pregunta real aquí es "¿Hay algo más obviamente malo o tortuoso con ese enfoque?"
Mi primer pensamiento aquí fue que esto no tiene sentido. Si su constructor nunca termina, ninguna otra parte del programa puede obtener una referencia al objeto construido, entonces, ¿cuál es la lógica detrás de colocarlo en un constructor en lugar de un método regular? Entonces se me ocurrió que la única diferencia sería que en su bucle podría permitir referencias a un
this
escape parcialmente construido del bucle. Si bien esto no se limita estrictamente a esta situación, se garantiza que si permitethis
escapar, 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 quieran profundizar.
fuente
this
en el constructor, pero solo si está en el constructor de la clase más derivada. Si tiene dos clases,A
yB
conB : A
, si el constructor deA
se filtrarathis
, los miembros deB
todavía no se inicializarían (y las llamadas o conversiones de métodos virtualesB
pueden causar estragos en función de eso).+1 Para el menor asombro, pero este es un concepto difícil de articular para los nuevos desarrolladores. A nivel pragmático, es difícil depurar las excepciones generadas dentro de los constructores, si el objeto no se inicializa, no existirá para que pueda inspeccionar el estado o iniciar sesión desde fuera de ese constructor.
Si siente la necesidad de hacer este tipo de patrón de código, utilice métodos estáticos en la clase.
Existe un constructor para proporcionar la lógica de inicialización cuando un objeto se instancia desde una definición de clase. Construye esta instancia de objeto porque es un contenedor que encapsula un conjunto de propiedades y funcionalidades que desea llamar desde el resto de la lógica de su aplicación.
Si no tiene la intención de utilizar el objeto que está "construyendo", ¿cuál es el punto de instanciar el objeto en primer lugar? Tener un ciclo while (verdadero) en un constructor efectivamente significa que nunca tiene la intención de que se complete ...
C # es un lenguaje orientado a objetos muy rico con muchas construcciones y paradigmas diferentes para que explore, conozca sus herramientas y cuándo usarlas.
fuente
No hay nada inherentemente malo al respecto. Sin embargo, lo que será importante es la pregunta de por qué elegiste usar esta construcción. ¿Qué obtuviste haciendo esto?
En primer lugar, no voy a hablar sobre el uso
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, debe estar listo 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 evitar esto. En el caso general, no puede preasignar 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 desea poder asignar todo por adelantado y luego llamar al bucle del juego.
Una de las reglas generales en la programación es usar la herramienta más simple para el trabajo. No es una regla difícil y rápida, pero es muy útil. Si una llamada a la 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, supongo que el desarrollador tenía una razón para ello, y comenzaré a explorar qué tipo de trucos extraños podrías estar haciendo.
Hay casos en que esto podría ser razonable. Puede haber casos en los que sea realmente intuitivo tener un bucle de juego en el constructor. En esos casos, ¡corres con eso! Por ejemplo, su ciclo 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 que el próximo desarrollador entienda lo que hizo y por qué.
fuente
GameTestResults testResults = runGameTests(tests, configuration);
en lugar deGameTestResults testResults = new GameTestResults(tests, configuration);
.Mi impresión es que algunos conceptos se mezclaron y dieron lugar a una pregunta no tan bien redactada.
El constructor de una clase puede iniciar un nuevo hilo que "nunca" terminará (aunque prefiero usar
while(_KeepRunning)
overwhile(true)
porque puedo configurar el miembro booleano como falso en alguna parte).Podría extraer ese método de crear e iniciar el subproceso en una función propia, para separar la construcción de objetos y el inicio real de su trabajo; prefiero esta forma porque obtengo un mejor control sobre el acceso a los recursos.
También puede echar un vistazo al "Patrón de objeto activo". Al final, supongo 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.
fuente
Su objeto me recuerda a iniciar Tareas . 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
yTask.Factory.StartNew
.Estos son muy similares a lo que está tratando de hacer, y es probable que esta sea la 'convención'. El problema principal que la gente parece tener es que este uso de un constructor los sorprende. @ JörgWMittag dice que rompe un contrato fundamental , lo que supongo que 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 con métodos de fábrica estáticos. La sorpresa se desvanece y no hay un contrato fundamental en juego aquí. Las personas están acostumbradas a los métodos estáticos de fábrica que hacen cosas especiales, y se les puede nombrar en consecuencia.
Puede proporcionar constructores que permitan un control detallado (como sugiere @Brandin, algo así
var g = new Game {...}; g.MainLoop();
, que se adapta a los usuarios que no desean comenzar el juego de inmediato, y tal vez quieran pasarlo primero. Y puede escribir algo comovar runningGame = Game.StartNew();
comenzar Es inmediatamente fácil.fuente
Eso no está nada mal.
¿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 mencionó algunas de las razones obvias en su contra, pero omitió la más importante:
Hay opciones mejores y más simples para lograr lo mismo.
Si hay 2 opciones y una es mejor, no importa cuánto sea mejor, usted elige la mejor. Por cierto, es por eso
quecasi nunca usamos GoTo.fuente
El propósito de un constructor es, bueno, construir su objeto. Antes de que el constructor haya completado, en principio es inseguro usar cualquier método de ese objeto. Por supuesto, podemos llamar métodos del objeto dentro del constructor, pero luego cada vez que lo hagamos, tenemos que asegurarnos de que hacerlo sea válido para el objeto en su estado actual de medio cocción.
Al poner indirectamente toda la lógica en el constructor, agrega más carga de este tipo sobre usted. Esto sugiere que no diseñó la diferencia entre inicialización y uso lo suficientemente bien.
fuente