¿Cómo puedo administrar la base de código de software significativamente complejo?


8

A menudo creo programas tanto para mí como para otros usando varios lenguajes de programación orientados a objetos. Al hacerlo, generalmente son relativamente pequeños (unos pocos miles de líneas como máximo). Recientemente, sin embargo, he estado intentando hacer proyectos más grandes, como motores de juegos completos. Al hacerlo, a menudo me encuentro con un obstáculo: la complejidad.

En mis proyectos más pequeños, es fácil recordar un mapa mental de cómo funciona cada parte del programa. Al hacer esto, puedo ser plenamente consciente de cómo cualquier cambio afectará al resto del programa y evitar errores de manera muy efectiva, así como ver exactamente cómo una nueva característica debe encajar en la base del código. Sin embargo, cuando intento crear proyectos más grandes, me resulta imposible mantener un buen mapa mental que conduzca a un código muy desordenado y numerosos errores no intencionados.

Además de este problema del "mapa mental", me resulta difícil mantener mi código desacoplado de otras partes de sí mismo. Por ejemplo, si en un juego multijugador hay una clase para manejar la física del movimiento de los jugadores y otra para manejar las redes, entonces no veo forma de que una de estas clases no dependa de la otra para llevar los datos de movimiento de los jugadores al sistema de redes para enviarlo a través de la red. Este acoplamiento es una fuente importante de la complejidad que interfiere con un buen mapa mental.

Por último, a menudo me encuentro con una o más clases de "gerente" que coordinan otras clases. Por ejemplo, en un juego, una clase manejaría el ciclo principal y llamaría a métodos de actualización en las clases de redes y jugadores. Esto va en contra de una filosofía de lo que he encontrado en mi investigación de que cada clase debe ser comprobable por unidad y utilizable independientemente de los demás, ya que cualquier clase de administrador por su propio propósito se basa en la mayoría de las otras clases en el proyecto. Además, una orquestación de clases de gerente del resto del programa es una fuente importante de complejidad no mapeable mental.

En conjunto, esto me impide escribir software libre de errores de alta calidad de un tamaño considerable. ¿Qué hacen los desarrolladores profesionales para tratar este problema de manera efectiva? Estoy especialmente interesado en el objetivo de respuestas OOP en Java y C ++, pero este tipo de consejo es probablemente muy general.

Notas:

  • He intentado usar diagramas UML, pero eso solo parece ayudar con el primer problema, e incluso solo cuando se trata de la estructura de clases en lugar de (por ejemplo) ordenar las llamadas a métodos con respecto a lo que se inicializa primero.

77
"Al negociar acciones, ¿cómo gano tanto dinero como sea posible?" - misma pregunta, ocupación diferente.
MaxB

1
¿Cuánto tiempo pasas en diseño y modelado? Muchos de los problemas con los que tropezamos durante el desarrollo provienen de una fuga de perspectiva. El tipo de perspectiva en la que nos enfocamos durante el modelado. Luego, intentamos resolver esta fuga con código. Código que también pierde fuga de perspectiva (la abstracción adecuada). Los proyectos a gran escala necesitan planificación, para tener una imagen global clara. Desde el nivel de la base de código será difícil tener la visión. " No dejes que los árboles te impidan ver el bosque ".
Laiv

44
Modularización y aislamiento. Pero parece que ya lo estás haciendo, pero no lo logras. En cuyo caso, realmente depende de qué tipo de proyecto es y qué tipo de tecnologías se utilizan.
Eufórico

1
La refactorización constante es una parte necesaria del desarrollo de software; considérelo como una indicación de que está haciendo algo bien. Cualquier sistema no trivial siempre contiene "incógnitas desconocidas", es decir, las cosas que no podría haber previsto ni explicado en su diseño original. La refactorización es la realidad del desarrollo iterativo y la evolución gradual hacia una solución "buena". Es la razón por la cual los desarrolladores tienden a favorecer la escritura de código que es fácil de refactorizar, lo que a menudo implica seguir los Principios SÓLIDOS
Ben Cottrell

2
UML IMHO carece del tipo de diagrama más útil para esto: diagramas de flujo de datos. Sin embargo, eso no es una bala de plata, la modularización y el aislamiento no vienen automáticamente mediante el uso de diagramas de flujo de datos.
Doc Brown

Respuestas:


8

A menudo parece que me encuentro con un obstáculo: la complejidad.

Hay libros enteros escritos sobre este tema. Aquí hay una cita de uno de los libros más importantes jamás escritos sobre desarrollo de software, el Código completo de Steve McConnell :

La gestión de la complejidad es el tema técnico más importante en el desarrollo de software. Desde mi punto de vista, es tan importante que el imperativo técnico primario del software tenga que gestionar la complejidad.

Por otro lado, recomendaría leer el libro si tiene algún interés en el desarrollo de software (lo cual supongo que sí, ya que ha hecho esta pregunta). Como mínimo, haga clic en el enlace de arriba y lea el extracto sobre conceptos de diseño.


Por ejemplo, si en un juego multijugador hay una clase para manejar la física del movimiento de los jugadores y otra para manejar las redes, entonces no veo forma de que una de estas clases no dependa de la otra para llevar los datos de movimiento de los jugadores al sistema de redes para enviarlo a través de la red.

En este caso particular, consideraría que su PlayerMovementCalculatorclase y su NetworkSystemclase no están relacionadas entre sí; una clase es responsable de calcular el movimiento del jugador y la otra es responsable de la E / S de la red. Quizás incluso en módulos independientes separados.

Sin embargo, ciertamente esperaría que haya al menos un poco de cableado adicional o pegamento en algún lugar fuera de esos módulos que median datos y / o eventos / mensajes entre ellos. Por ejemplo, puede escribir una PlayerNetworkMediatorclase utilizando el Patrón de mediador .

Otro enfoque posible podría ser desacoplar sus módulos usando un Agregador de eventos .

En el caso de la programación asincrónica , como el tipo de lógica involucrada con los sockets de red, puede usar expose Observables para ordenar el código que 'escucha' esas notificaciones.

La programación asincrónica tampoco significa necesariamente multiproceso; se trata más de la estructura del programa y el control de flujo (aunque el subproceso múltiple es el caso de uso obvio para la asincronía). Los observables pueden ser útiles en uno o ambos de esos módulos para permitir que las clases no relacionadas se suscriban para cambiar las notificaciones.

Por ejemplo:

  • NetworkMessageReceivedEvent
  • PlayerPositionChangedEvent
  • PlayerDisconnectedEvent

etc.


Por último, a menudo me encuentro con una o más clases de "gerente" que coordinan otras clases. Por ejemplo, en un juego, una clase manejaría el ciclo principal y llamaría a métodos de actualización en las clases de redes y jugadores. Esto va en contra de una filosofía de lo que he encontrado en mi investigación de que cada clase debe ser comprobable por unidad y utilizable independientemente de los demás, ya que cualquier clase de administrador por su propio propósito se basa en la mayoría de las otras clases en el proyecto. Además, una orquestación de clases de gerente del resto del programa es una fuente importante de complejidad no mapeable mental.

Si bien algo de esto ciertamente se reduce a la experiencia; El nombre Manageren una clase a menudo indica un olor de diseño .

Al nombrar clases, considere la funcionalidad de la que es responsable la clase y permita que los nombres de sus clases reflejen lo que hace .

El problema con los gerentes en el código es un poco como el problema con los gerentes en el lugar de trabajo. Su propósito tiende a ser vago y pobremente entendido, incluso por sí mismos; la mayoría de las veces estamos mejor sin ellos por completo.

La programación orientada a objetos se trata principalmente de comportamiento . Una clase no es una entidad de datos, sino una representación de algún requisito funcional en su código.

Si puede nombrar una clase en función del requisito funcional que cumple, reducirá sus posibilidades de terminar con algún tipo de objeto de Dios hinchado , y es más probable que tenga una clase cuya identidad y propósito en su programa sea clara.

Además, debería ser más obvio cuando los métodos y comportamientos adicionales comienzan a aparecer cuando realmente no pertenece, porque el nombre comenzará a verse mal, es decir, tendrá una clase que está haciendo un montón de cosas que no son t reflejado por su nombre

Por último, evite la tentación de escribir clases cuyos nombres parezcan pertenecer a un modelo de relación de entidad. El problema con los nombres de las clases, tales como Player, Monster, Car, Dog, etc es que el nada sobre su comportamiento implican, y sólo parecen describir un conjunto de datos o atributos relacionados lógicamente. El diseño orientado a objetos no es el modelado de datos, es el modelado de comportamiento.

Por ejemplo, considere dos maneras diferentes de modelar una Monstery Playercálculo de daños:

class Monster : GameEntity {
    dealDamage(...);
}

class Player : GameEntity {
    dealDamage(...);
}

El problema aquí es que puede esperar razonablemente Playery Monstertener un montón de otros métodos que probablemente no estén relacionados con la cantidad de daño que estas entidades podrían hacer (Movimiento, por ejemplo); estás en el camino hacia el objeto de Dios mencionado anteriormente.

Un enfoque más natural orientado a objetos es identificar el nombre de la clase en función de su comportamiento, por ejemplo:

class MonsterDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

class PlayerDamageDealer : IDamageDealer {
    dealDamage(...) { }
}

Con este tipo de diseño, sus objetos Playery Monsterprobablemente no tengan ningún método asociado con ellos porque esos objetos contienen los datos necesarios para toda su aplicación; probablemente sean simples entidades de datos que viven dentro de un repositorio y solo contienen campos / propiedades.

Este enfoque generalmente se conoce como modelo de dominio anémico , que se considera un antipatrón para el diseño impulsado por dominio (DDD) , pero los principios SÓLIDOS naturalmente lo llevan a una separación limpia entre entidades de datos 'compartidas' (tal vez en un repositorio) y clases de comportamiento modulares (preferiblemente sin estado) en el gráfico de objetos de su aplicación.

SOLID y DDD son dos enfoques diferentes para el diseño OO; Si bien se cruzan de muchas maneras, tienden a tomar direcciones opuestas con respecto a la identidad de clase y la separación de datos y comportamiento.


Volviendo a la cita anterior de McConnell: la gestión de la complejidad es la razón por la cual el desarrollo de software es una profesión calificada en lugar de una tarea administrativa mundana. Antes de que McConnell escribiera su libro, Fred Brooks escribió un artículo sobre el tema que resume claramente la respuesta a su pregunta: No hay una bala de plata para manejar la complejidad.

Entonces, si bien no hay una respuesta única, puede hacer la vida más fácil o más difícil para usted dependiendo de la forma en que la aborde:

  • Recuerda KISS , DRY y YAGNI .
  • Comprenda cómo aplicar los principios SÓLIDOS del diseño OO / desarrollo de software
  • También comprenda el diseño impulsado por dominio, incluso si hay lugares donde el enfoque entra en conflicto con los principios SOLIDOS; SOLID y DDD tienden a estar más de acuerdo que en desacuerdo.
  • Espere que su código cambie: escriba pruebas automatizadas para captar las consecuencias de esos cambios (no tiene que seguir TDD para escribir pruebas automatizadas útiles ; de hecho, algunas de esas pruebas podrían ser pruebas de integración utilizando aplicaciones de consola "desechables" o prueba de aplicaciones de arnés)
  • Lo más importante: ser pragmático. No sigas servilmente ninguna guía; lo contrario de la complejidad es la simplicidad, por lo que si tiene dudas (de nuevo) - KISS

Respuesta sólida Juego de palabras previsto.
ipaul

44
No estoy de acuerdo con que DDD y SOLID estén en desacuerdo entre sí. Creo que son bastante complementarios de hecho. Parte de por qué pueden parecer contradictorios, o quizás ortogonales, es que describen cosas diferentes en un nivel diferente de abstracción.
RibaldEddie

1
@RibaldEddie Estoy de acuerdo en que son complementarios en su mayor parte; El problema principal que veo es que SOLID parece alentar activamente el llamado modelo de dominio "anémico" como una consecuencia natural de llevar a SRP a su conclusión lógica de una verdadera responsabilidad única por clase, mientras que DDD lo llama un anti modelo. ¿O tal vez hay algo en la descripción de Fowler del modelo de dominio anémico que he interpretado mal?
Ben Cottrell

1

Creo que se trata de gestionar la complejidad, que siempre se reduce cuando las cosas a su alrededor se vuelven demasiado complejas para seguir manejando con lo que tiene actualmente y probablemente ocurre un par de veces al crecer de una empresa de 1 hombre a una empresa global de medio millón de personas. .

En general, la complejidad crece cuando crece su proyecto. Y, en general, en el momento en que la complejidad se vuelve demasiado (independiente si se trata de TI o la administración del programa, los programas o la empresa completa) necesita más herramientas o reemplazar herramientas con herramientas que manejan mejor la complejidad, que es lo que la humanidad ha hecho desde siempre. Y las herramientas van de la mano con procesos más complejos, ya que alguien necesita generar algo en lo que otra persona necesita trabajar. Y estos van de la mano con "personas adicionales" que tienen roles específicos, por ejemplo, un arquitecto de empresa que no es necesario con un proyecto de 1 persona en el hogar.

Al mismo tiempo, su taxonomía debe crecer detrás de su complejidad hasta que llegue el momento en que la taxonomía sea demasiado compleja y pase a los datos no estructurados. Por ejemplo, puede hacer una lista de sitios web y clasificarlos, pero si necesita tener una lista de mil millones de sitios web, no hay forma de hacerlo dentro de un tiempo razonable, de modo que vaya a algoritmos más inteligentes o simplemente busque datos.

En un proyecto pequeño, puede trabajar en una unidad compartida. En un proyecto algo grande, instala .git o .svn o lo que sea. En un programa complejo con múltiples proyectos que tienen diferentes versiones, todas están en vivo y todas tienen diferentes fechas de lanzamiento y todas dependen de otros sistemas, es posible que deba cambiar a clearcase si es responsable de la administración de la configuración de todo en lugar de simplemente versionar Algunas ramas de un proyecto.

Así que creo que el mejor esfuerzo de los humanos hasta ahora es una combinación de herramientas, roles específicos, procesos para gestionar la creciente complejidad de casi todo.

Afortunadamente, hay empresas que venden marcos de negocios, marcos de procesos, taxonomías, modelos de datos, etc., por lo que dependiendo de la industria en la que se encuentre puede comprar, cuando la complejidad ha crecido tanto que todos reconocen que no hay otra manera, un modele y procese de manera inmediata para, por ejemplo, una compañía de seguros global, desde allí usted trabaja para refactorizar sus aplicaciones existentes para volver a alinear todo con el marco general probado que ya incluye un modelo de datos y procesos probados.

Si no hay ninguno en el mercado para su industria, tal vez haya una oportunidad :)


Parece que básicamente estás sugiriendo una combinación de "contratar personas" o "comprar algo que hará que tus problemas desaparezcan ™". Estoy interesado en gestionar la complejidad como desarrollador individual sin presupuesto que trabaje en proyectos de hobby (por ahora).
john01dav

Creo que, en ese nivel, probablemente su pregunta sea algo así como "nombre 100 mejores prácticas que aplica un buen arquitecto de aplicaciones". Porque creo que, en esencia, está manejando la complejidad detrás de todo lo que hace. él establece estándares para combatir la complejidad.
edelwater

Eso es algo de lo que estoy preguntando, pero estoy más interesado en una filosofía general que en una lista.
john01dav

Es una pregunta amplia, ya que hay 21.000+ libros sobre estudios de arquitectura de aplicaciones / patrones / diseño, etc: safaribooksonline.com/search/?query=application%20architecture
edelwater

El libro solo puede mostrar la popularidad de una pregunta, no la amplitud de las buenas respuestas. Si cree que se necesita un libro para responder a esta pregunta, haga una recomendación.
john01dav

0

Deja de escribir todo OOP y agrega algunos servicios en su lugar.

Por ejemplo, hace poco escribí un GOAP para un juego. Verá los ejemplos en Internet uniendo sus acciones al motor del juego con el estilo OOP:

Action.Process();

En mi caso, necesitaba procesar acciones tanto en el motor como también fuera del motor, simulando acciones de una manera menos detallada donde el jugador no está presente. Entonces en su lugar usé:

Interface IActionProcessor<TAction>
{
    void ProcessAction(TAction action);
}

Esto me permite desacoplar la lógica y tener dos clases.

ActionProcesser_InEngine.ProcessAction()


ActionProcesser_BackEnd.ProcessAction()

No es POO, pero significa que las acciones ahora no tienen ningún enlace con el motor. Por lo tanto, mi módulo GOAP está completamente separado del resto del juego.

Una vez terminado, solo uso el dll y no pienso en la implementación interna.


1
¿Cómo es que su procesador de acción no está orientado a objetos?
Greg Burghardt

2
Creo que su punto es: “No intente producir un diseño de OOP, porque OOP es solo una de las muchas técnicas para abordar la complejidad; algo más (por ejemplo, un enfoque de procedimiento) podría funcionar mejor en su caso. No importa cómo llegues allí, siempre y cuando tengas límites claros de módulo que resuman los detalles de la implementación ”.
amon

1
Creo que el problema no está a nivel de código. Está en niveles más altos. Los problemas con el código base son síntomas, no enfermedades. La solución no vendrá del código. Creo que vendrá del diseño y el modelado.
Laiv

2
Si leo esto imaginándome a mí mismo como el OP, encuentro que esta respuesta carece de detalles y no proporciona ninguna información útil, ya que no describe cómo el IActionProcessor realmente proporciona valor en lugar de simplemente agregar otra capa de indirección.
whatsisname

usar servicios como este es de procedimiento en lugar de OO. Esto proporciona una separación más clara entre las responsabilidades de OO, que como la OP ha encontrado puede rápidamente llegar a ser excesivamente acoplado
Ewan

0

¡Qué gran carga de preguntas! Podría avergonzarme intentando este con mis pensamientos extravagantes (y me encantaría escuchar sugerencias si realmente estoy fuera). Pero para mí, lo más útil que he aprendido últimamente en mi dominio (que incluía los juegos en el pasado, ahora VFX) ha sido reemplazar las interacciones entre interfaces abstractas con datos como un mecanismo de desacoplamiento (y finalmente reducir la cantidad de información requerida entre cosas y unos de otros al mínimo absoluto). Esto puede sonar completamente loco (y podría estar usando todo tipo de terminología deficiente).

Sin embargo, digamos que te doy un trabajo razonablemente manejable. Tiene este archivo que contiene datos de escena y animación para renderizar. Hay documentación que cubre el formato del archivo. Su único trabajo es cargar el archivo, renderizar imágenes bonitas para la animación usando el trazado de ruta y generar los resultados en archivos de imagen. Esa es una aplicación de pequeña escala que probablemente no abarcará más de decenas de miles de LOC incluso para un renderizador bastante sofisticado (definitivamente no millones).

ingrese la descripción de la imagen aquí

Tienes tu propio pequeño mundo aislado para este renderizador. No se ve afectado por el mundo exterior. Aísla su propia complejidad. Más allá de las preocupaciones de leer este archivo de escena y enviar sus resultados a archivos de imagen, puede enfocarse por completo en el renderizado. Si algo sale mal en el proceso, sabes que está en el renderizador y nada más, ya que no hay nada más involucrado en esta imagen.

Mientras tanto, digamos que debe hacer que su renderizador funcione en el contexto de un gran software de animación que en realidad tiene millones de LOC. En lugar de simplemente leer un formato de archivo simplificado y documentado para obtener los datos necesarios para la representación, debe pasar por todo tipo de interfaces abstractas para recuperar todos los datos que necesita para hacer lo suyo:

ingrese la descripción de la imagen aquí

De repente, tu renderizador ya no está en su propio pequeño mundo aislado. Esto se siente mucho, mucho más complejo. Debe comprender el diseño general de una gran parte del software como un todo orgánico con potencialmente muchas partes móviles, y tal vez incluso a veces tenga que pensar en implementaciones de elementos como mallas o cámaras si golpea un cuello de botella o un error en uno de Las funciones.

Funcionalidad versus datos optimizados

Y una de las razones es porque la funcionalidad es mucho más compleja que los datos estáticos. También hay tantas formas en que una llamada a la función podría salir mal de una manera que la lectura de datos estáticos no puede. Hay tantos efectos secundarios ocultos que pueden ocurrir al llamar a esas funciones a pesar de que conceptualmente solo está recuperando datos de solo lectura para renderizar. También puede tener muchas más razones para cambiar. En unos pocos meses a partir de ahora, es posible que la interfaz de malla o textura cambie o desaproveche partes de tal forma que requiera reescribir secciones importantes de su renderizador y mantenerse al día con esos cambios, aunque esté obteniendo exactamente los mismos datos, aunque la entrada de datos a su procesador no ha cambiado en absoluto (solo la funcionalidad requerida para acceder a todo).

Entonces, cuando es posible, descubrí que los datos simplificados son un mecanismo de desacoplamiento muy bueno de un tipo que realmente le permite evitar tener que pensar en todo el sistema en su conjunto y le permite concentrarse en una parte muy específica del sistema para hacer mejoras, agregar nuevas funciones, arreglar cosas, etc. Sigue una mentalidad muy de E / S para las piezas voluminosas que componen su software. Ingrese esto, haga lo suyo, elabore eso y, sin pasar por docenas de interfaces abstractas, finalice llamadas de función sin fin en el camino. Y está empezando a parecerse, hasta cierto punto, a la programación funcional.

Entonces, esta es solo una estrategia y puede no ser aplicable para todas las personas. Y, por supuesto, si está volando solo, todavía tiene que mantener todo (incluido el formato de los datos en sí), pero la diferencia es que cuando se sienta para hacer mejoras en ese renderizador, realmente puede concentrarse en el renderizador en su mayor parte y nada más. Se vuelve tan aislado en su propio pequeño mundo, casi tan aislado como podría estar con los datos que requiere para que la entrada sea tan simple.

Y utilicé el ejemplo de un formato de archivo, pero no tiene que ser un archivo que proporcione los datos simplificados de interés para la entrada. Podría ser una base de datos en memoria. En mi caso, es un sistema de entidad-componente con los componentes que almacenan los datos de interés. Sin embargo, he encontrado que este principio básico de desacoplamiento hacia los datos simplificados (sin importar cómo lo hagas) es mucho menos exigente para mi capacidad mental que los sistemas anteriores en los que trabajé, que giraban en torno a abstracciones y montones y montones de interacciones entre todos estos interfaces abstractas que hacían imposible simplemente sentarse con una cosa y pensar solo en eso y poco más. Mi cerebro se llenó hasta el borde con esos tipos de sistemas anteriores y quería explotar porque había muchas interacciones entre tantas cosas,

Desacoplamiento

Si desea minimizar la cantidad de bases de código más grandes que gravan su cerebro, haga que cada parte considerable del software (un sistema de representación completo, un sistema de física completo, etc.) viva en el mundo más aislado posible. Minimice la cantidad de comunicación e interacción que pasa a los mínimos más mínimos a través de los datos más optimizados. Incluso podría aceptar cierta redundancia (algún trabajo redundante para el procesador, o incluso para usted mismo) si el intercambio es un sistema mucho más aislado que no tiene que hablar con docenas de otras cosas antes de que pueda hacer su trabajo.

Y cuando comienzas a hacer eso, parece que estás manteniendo una docena de aplicaciones a pequeña escala en lugar de una gigantesca. Y eso me parece mucho más divertido también. Puedes sentarte y trabajar en un sistema a tu gusto sin preocuparte por el mundo exterior. Simplemente se convierte en la entrada de los datos correctos y la salida de los datos correctos al final a algún lugar donde otros sistemas puedan acceder (en ese momento, otro sistema podría ingresar eso y hacer lo suyo, pero no tiene que preocuparse por eso cuando trabajas en tu sistema). Por supuesto, todavía tiene que pensar en cómo se integra todo en la interfaz de usuario, por ejemplo (todavía me encuentro teniendo que pensar en el diseño de todo para las GUI), pero al menos no cuando se sienta y trabaja en ese sistema existente. o decide agregar uno nuevo.

Quizás estoy describiendo algo obvio para las personas que se mantienen actualizadas con los últimos métodos de ingeniería. No lo sé. Pero no fue obvio para mí. Quería abordar el diseño de software en torno a objetos que interactúan entre sí y a las funciones que se requieren para software a gran escala. Y los libros que leí originalmente sobre diseño de software a gran escala se centraron en diseños de interfaz por encima de cosas como implementaciones y datos (el mantra en ese entonces era que las implementaciones no importan tanto, solo las interfaces, porque las primeras podrían intercambiarse fácilmente o sustituirse ) Al principio, no se me ocurrió intuitivamente pensar que las interacciones de un software se reducen a solo ingresar y emitir datos entre enormes subsistemas que apenas se comunican entre sí, excepto a través de estos datos simplificados. Sin embargo, cuando comencé a cambiar mi enfoque al diseño en torno a ese concepto, hizo las cosas mucho más fáciles. Podría agregar mucho más código sin que mi cerebro explote. Se sentía como si estuviera construyendo un centro comercial en lugar de una torre que podría derrumbarse si agregué demasiado o si hubo una fractura en alguna parte.

Implementaciones complejas versus interacciones complejas

Este es otro que debo mencionar porque pasé una buena parte de mi primera parte de mi carrera buscando las implementaciones más simples. Así que descompuse las cosas en los pedazos más pequeños y simples, pensando que estaba mejorando la capacidad de mantenimiento.

En retrospectiva, no me di cuenta de que estaba cambiando un tipo de complejidad por otro. Al reducir todo a las partes más simples, las interacciones que ocurrieron entre esas piezas pequeñas se convirtieron en la red más compleja de interacciones con llamadas a funciones que a veces fueron 30 niveles de profundidad en la pila de llamadas. Y, por supuesto, si observa cualquier función, es muy, muy simple y fácil saber lo que hace. Pero no está obteniendo mucha información útil en ese punto porque cada función está haciendo muy poco. Luego, terminas teniendo que rastrear todo tipo de funciones y saltar a través de todo tipo de aros para descubrir realmente lo que suman hacer de maneras que pueden hacer que tu cerebro quiera explotar más de uno más grande,

Eso no es sugerir objetos de Dios ni nada de eso. Pero tal vez no necesitemos dividir nuestros objetos de malla en las cosas más pequeñas, como un objeto de vértice, un objeto de borde, un objeto de cara. Tal vez podríamos mantenerlo en "malla" con una implementación moderadamente más compleja detrás de él a cambio de radicalmente menos interacciones de código. Puedo manejar una implementación moderadamente compleja aquí y allá. No puedo manejar millones de interacciones con efectos secundarios que ocurren quién sabe dónde y en qué orden.

Al menos eso me resulta mucho, mucho menos exigente para el cerebro, porque son las interacciones las que hacen que mi cerebro duela en una gran base de código. No hay una cosa específica.

Generalidad versus especificidad

Tal vez atado a lo anterior, me encantaba la generalidad y la reutilización de código, y solía pensar que el mayor desafío de diseñar una buena interfaz era satisfacer la más amplia gama de necesidades porque la interfaz sería utilizada por todo tipo de cosas diferentes con diferentes necesidades. Y cuando haces eso, inevitablemente tienes que pensar en cien cosas a la vez, porque estás tratando de equilibrar las necesidades de cien cosas a la vez.

Generalizar las cosas lleva mucho tiempo. Basta con mirar las bibliotecas estándar que acompañan a nuestros idiomas. La biblioteca estándar de C ++ contiene muy poca funcionalidad, pero requiere equipos de personas para mantener y sintonizar con comités enteros de personas que debaten y hacen propuestas sobre su diseño. Eso se debe a que esa pequeña funcionalidad está tratando de manejar las necesidades del mundo entero.

Quizás no necesitamos llevar las cosas tan lejos. Quizás esté bien tener un índice espacial que solo se use para la detección de colisiones entre mallas indexadas y nada más. Tal vez podamos usar otro para otros tipos de superficies, y otro para renderizar. Solía ​​centrarme tanto en eliminar este tipo de redundancias, pero parte de la razón era porque estaba tratando con estructuras de datos muy ineficientes implementadas por una amplia gama de personas. Naturalmente, si tiene un octree que toma 1 gigabyte por una simple malla triangular de 300k, no querrá tener otro más en la memoria.

Pero, ¿por qué los octrees son tan ineficientes en primer lugar? Puedo crear octreos que solo toman 4 bytes por nodo y toman menos de un megabyte para hacer lo mismo que la versión de gigabytes mientras construyen en una fracción del tiempo y realizan consultas de búsqueda más rápidas. En ese punto, algo de redundancia es totalmente aceptable.

Eficiencia

Por lo tanto, esto solo es relevante para los campos críticos para el rendimiento, pero cuanto mejor obtenga en cosas como la eficiencia de la memoria, más podrá permitirse desperdiciar un poco más (tal vez acepte un poco más de redundancia a cambio de una menor generalidad o desacoplamiento) a favor de la productividad . Y allí ayuda a ser bastante bueno y cómodo con sus perfiladores y aprender sobre la arquitectura de la computadora y la jerarquía de la memoria, porque entonces puede permitirse hacer más sacrificios a la eficiencia a cambio de la productividad porque su código ya es tan eficiente y puede permitirse el lujo de ser un poco menos eficiente, incluso en las áreas críticas, mientras sigue superando a la competencia. Descubrí que mejorar en esta área también me ha permitido salir con implementaciones más y más simples,

Fiabilidad

Esto es algo obvio, pero bien podría mencionarlo. Sus cosas más confiables requieren el mínimo gasto intelectual. No tienes que pensar mucho en ellos. Ellos solo trabajan. Como resultado, cuanto más grande sea su lista de piezas ultra confiables que también son "estables" (no es necesario cambiar) a través de pruebas exhaustivas, menos tendrá que pensar.

Detalles específicos

Entonces, todo lo anterior cubre algunas cosas generales que me han sido útiles, pero pasemos a aspectos más específicos para su área:

En mis proyectos más pequeños, es fácil recordar un mapa mental de cómo funciona cada parte del programa. Al hacer esto, puedo ser plenamente consciente de cómo cualquier cambio afectará al resto del programa y evitar errores de manera muy efectiva, así como ver exactamente cómo una nueva característica debe encajar en la base del código. Sin embargo, cuando intento crear proyectos más grandes, me resulta imposible mantener un buen mapa mental que conduzca a un código muy desordenado y numerosos errores no intencionados.

Para mí, esto tiende a estar relacionado con efectos secundarios complejos y flujos de control complejos. Esa es una vista de nivel bastante bajo de las cosas, pero todas las interfaces más bonitas y todo el desacoplamiento del concreto al abstracto no pueden hacer que sea más fácil razonar sobre los efectos secundarios complejos que ocurren en los flujos de control complejos.

Simplifique / reduzca los efectos secundarios y / o simplifique los flujos de control, idealmente ambos. y generalmente le resultará mucho más fácil razonar sobre lo que hacen los sistemas mucho más grandes, y también sobre lo que sucederá en respuesta a sus cambios.

Además de este problema del "mapa mental", me resulta difícil mantener mi código desacoplado de otras partes de sí mismo. Por ejemplo, si en un juego multijugador hay una clase para manejar la física del movimiento de los jugadores y otra para manejar las redes, entonces no veo forma de que una de estas clases no dependa de la otra para llevar los datos de movimiento de los jugadores al sistema de redes para enviarlo a través de la red. Este acoplamiento es una fuente importante de la complejidad que interfiere con un buen mapa mental.

Conceptualmente tienes que tener algo de acoplamiento. Cuando las personas hablan de desacoplamiento, generalmente significan reemplazar un tipo con otro, un tipo más deseable (generalmente hacia abstracciones). Para mí, dado mi dominio, la forma en que funciona mi cerebro, etc., el tipo más deseable para reducir los requisitos de "mapa mental" a un mínimo es que los datos simplificados discutidos anteriormente. Un recuadro negro escupe datos que se introducen en otro recuadro negro, y ambos completamente ajenos a la existencia del otro. Solo conocen algún lugar central donde se almacenan los datos (por ejemplo: un sistema de archivos central o una base de datos central) a través del cual obtienen sus entradas, hacen algo y escupen una nueva salida que luego podría ingresar otra caja negra.

Si lo hace de esta manera, el sistema de física dependería de la base de datos central y el sistema de red dependería de la base de datos central, pero no sabrían nada el uno del otro. Ni siquiera tendrían que conocerse entre sí. Ni siquiera tendrían que saber que existen interfaces abstractas entre sí.

Por último, a menudo me encuentro con una o más clases de "gerente" que coordinan otras clases. Por ejemplo, en un juego, una clase manejaría el ciclo principal y llamaría a métodos de actualización en las clases de redes y jugadores. Esto va en contra de una filosofía de lo que he encontrado en mi investigación de que cada clase debe ser comprobable por unidad y utilizable independientemente de los demás, ya que cualquier clase de administrador por su propio propósito se basa en la mayoría de las otras clases en el proyecto. Además, una orquestación de clases de gerente del resto del programa es una fuente importante de complejidad no mapeable mental.

Tiende a necesitar algo para organizar todos los sistemas de su juego. Central es quizás al menos menos complejo y más manejable que como un sistema de física que invoca un sistema de renderizado una vez hecho. Pero aquí inevitablemente necesitamos que se invoquen algunas funciones, y preferiblemente son abstractas.

Por lo tanto, puede crear una interfaz abstracta para un sistema con una updatefunción abstracta . Luego puede registrarse con el motor central y su sistema de red puede decir: "Hola, soy un sistema y aquí está mi función de actualización. Llámeme de vez en cuando". Y luego su motor puede recorrer todos esos sistemas y actualizarlos sin necesidad de codificar llamadas de función a sistemas específicos.

Eso permite que sus sistemas vivan más como en su propio mundo aislado. El motor del juego ya no tiene que saber sobre ellos específicamente (de manera concreta). Y luego su sistema de física podría tener su función de actualización llamada, en cuyo punto ingresa los datos que necesita de la base de datos central para todo el movimiento, aplica la física y luego devuelve el movimiento resultante.

Después de eso, su sistema de red puede tener su función de actualización llamada, en cuyo punto ingresa los datos que necesita de la base de datos central y emite, por ejemplo, datos de socket a los clientes. Una vez más, el objetivo, como lo veo, es aislar cada sistema tanto como sea posible para que pueda vivir en su propio pequeño mundo con un conocimiento mínimo del mundo exterior. Ese es básicamente el tipo de enfoque adoptado en ECS que es popular entre los motores de juegos.

ECS

Supongo que debería cubrir ECS un poco, ya que muchos de mis pensamientos anteriores giran en torno a ECS y tratar de racionalizar por qué este enfoque orientado a datos para el desacoplamiento ha hecho que el mantenimiento sea mucho más fácil que los sistemas orientados a objetos y basados ​​en COM que he mantenido en el pasado a pesar de violar casi todo lo que consideraba sagrado originalmente que aprendí sobre SE. También podría tener mucho sentido para ti si estás tratando de construir juegos a gran escala. Entonces ECS funciona así:

ingrese la descripción de la imagen aquí

Y como en el diagrama anterior, la función MovementSystempodría tener updatellamada. En este punto, puede consultar los PosAndVelocitycomponentes de la base de datos central como los datos a ingresar (los componentes son solo datos, sin funcionalidad). Entonces podría recorrerlos, modificar las posiciones / velocidades y generar los nuevos resultados de manera efectiva. Luego, se RenderingSystempodría llamar a su función de actualización, en cuyo punto consulta la base de datos PosAndVelocityy los Spritecomponentes, y genera imágenes en la pantalla en función de esos datos.

Todos los sistemas son completamente ajenos a la existencia del otro, y ni siquiera necesitan entender qué Cares. Solo necesitan conocer componentes específicos del interés de cada sistema que componen los datos necesarios para representar uno. Cada sistema es como una caja negra. Introduce datos y genera datos con un conocimiento mínimo del mundo exterior, y el mundo exterior también tiene un conocimiento mínimo de él. Puede haber algún evento empujando desde un sistema y explotando desde otro para que, por ejemplo, la colisión de dos entidades en el sistema de física pueda hacer que el audio vea un evento de colisión que haga que suene, pero los sistemas aún pueden ser ajenos acerca de cada uno. Y he encontrado que tales sistemas son mucho más fáciles de razonar. No hacen que mi cerebro quiera explotar, incluso si tienes docenas de sistemas, porque cada uno está muy aislado. Usted no No tiene que pensar en la complejidad de todo en su conjunto cuando se acerca y trabaja en cualquiera. Y debido a eso, también es muy fácil predecir los resultados de sus cambios.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.