Su preocupación era que la gran cantidad de clases se traduciría en una pesadilla de mantenimiento. Mi opinión era que tendría exactamente el efecto contrario.
Estoy absolutamente del lado de tu amigo, pero podría ser una cuestión de nuestros dominios y los tipos de problemas y diseños que abordamos y especialmente qué tipos de cosas pueden requerir cambios en el futuro. Diferentes problemas, diferentes soluciones. No creo en lo correcto o incorrecto, solo en los programadores que intentan encontrar la mejor manera de resolver sus problemas de diseño particulares. Trabajo en VFX, que no es muy diferente de los motores de juego.
Pero el problema para mí con el que luché en lo que al menos podría llamarse algo más de una arquitectura de conformidad SÓLIDA (estaba basado en COM), podría resumirse en "demasiadas clases" o "demasiadas funciones" como tu amigo podría describirlo. Diría específicamente, "demasiadas interacciones, demasiados lugares que podrían comportarse mal, demasiados lugares que podrían causar efectos secundarios, demasiados lugares que podrían necesitar cambiar y demasiados lugares que podrían no hacer lo que creemos que hacen". ".
Tuvimos un puñado de interfaces abstractas (y puras) implementadas por una gran cantidad de subtipos, así (hicimos este diagrama en el contexto de hablar sobre los beneficios de ECS, sin tener en cuenta el comentario de abajo a la izquierda):
Donde una interfaz de movimiento o una interfaz de nodo de escena podría implementarse por cientos de subtipos: luces, cámaras, mallas, solucionadores físicos, sombreadores, texturas, huesos, formas primitivas, curvas, etc., etc. (y a menudo había múltiples tipos de cada uno ) Y el problema final era que esos diseños no eran tan estables. Tuvimos requisitos cambiantes y, a veces, las interfaces mismas tuvieron que cambiar, y cuando desea cambiar una interfaz abstracta implementada por 200 subtipos, es un cambio extremadamente costoso. Comenzamos a mitigar eso mediante el uso de clases base abstractas entre las cuales se redujeron los costos de dichos cambios de diseño, pero aún así eran caros.
Así que, alternativamente, comencé a explorar la arquitectura del sistema de componentes de entidad que se usa con bastante frecuencia en la industria del juego. Eso cambió todo para ser así:
Y guau! Esa fue una gran diferencia en términos de mantenibilidad. Las dependencias ya no fluían hacia abstracciones , sino hacia datos (componentes). Y al menos en mi caso, los datos eran mucho más estables y más fáciles de acertar en términos de diseño por adelantado a pesar de los requisitos cambiantes (aunque lo que podemos hacer con los mismos datos cambia constantemente con los requisitos cambiantes).
Además, debido a que las entidades en un ECS usan composición en lugar de herencia, en realidad no necesitan contener funcionalidad. Son simplemente el "contenedor de componentes" analógico. Eso hizo que los 200 subtipos analógicos que implementaron una interfaz de movimiento se conviertan en 200 instancias de entidad (no tipos separados con código separado) que simplemente almacenan un componente de movimiento (que no es más que datos asociados con el movimiento). A PointLight
ya no es una clase / subtipo separado. No es una clase en absoluto. Es una instancia de una entidad que solo combina algunos componentes (datos) relacionados con su ubicación en el espacio (movimiento) y las propiedades específicas de las luces de punto. La única funcionalidad asociada con ellos está dentro de los sistemas, como elRenderSystem
, que busca componentes de luz en la escena para determinar cómo representar la escena.
Con los requisitos cambiantes bajo el enfoque de ECS, a menudo solo era necesario cambiar uno o dos sistemas que funcionan con esos datos o simplemente introducir un nuevo sistema en el lateral, o introducir un nuevo componente si se necesitaban nuevos datos.
Entonces, al menos para mi dominio, y estoy casi seguro de que no es para todos, esto hizo las cosas mucho más fáciles porque las dependencias fluían hacia la estabilidad (cosas que no necesitaban cambiar a menudo). Ese no era el caso en la arquitectura COM cuando las dependencias fluían uniformemente hacia las abstracciones. En mi caso, es mucho más fácil averiguar qué datos se requieren para el movimiento por adelantado en lugar de todas las cosas posibles que podría hacer con él, que a menudo cambia un poco a lo largo de los meses o años a medida que entran nuevos requisitos.
¿Hay casos en OOP donde algunos o todos los principios SOLIDOS no se prestan para limpiar el código?
Bueno, no puedo decir código limpio, ya que algunas personas equiparan el código limpio con SOLID, pero definitivamente hay algunos casos en los que separar los datos de la funcionalidad como lo hace el ECS, y redirigir las dependencias de las abstracciones hacia los datos definitivamente puede hacer que las cosas sean mucho más fáciles. cambiar, por razones obvias de acoplamiento, si los datos van a ser mucho más estables que las abstracciones. Por supuesto, las dependencias de los datos pueden dificultar el mantenimiento de invariantes, pero ECS tiende a mitigarlo al mínimo con la organización del sistema, lo que minimiza el número de sistemas que acceden a cualquier tipo de componente.
No es necesariamente que las dependencias fluyan hacia abstracciones, como sugeriría DIP; las dependencias deben fluir hacia cosas que es muy poco probable que necesiten cambios futuros. Eso puede o no ser abstracciones en todos los casos (ciertamente no estaba en el mío).
- Sí, existen principios de diseño de OOP que están parcialmente en conflicto con SOLID
- Sí, existen principios de diseño de OOP que están completamente en conflicto con SOLID.
No estoy seguro de si ECS es realmente un sabor de OOP. Algunas personas lo definen de esa manera, pero lo veo muy diferente inherentemente con las características de acoplamiento y la separación de datos (componentes) de la funcionalidad (sistemas) y la falta de encapsulación de datos. Si se considera una forma de OOP, creo que está muy en conflicto con SOLID (al menos las ideas más estrictas de SRP, abierto / cerrado, sustitución de liskov y DIP). Pero espero que este sea un ejemplo razonable de un caso y dominio en el que los aspectos más fundamentales de SOLID, al menos como la gente generalmente los interpretaría en un contexto OOP más reconocible, podrían no ser tan aplicables.
Clases pequeñitas
Estaba explicando la arquitectura de uno de mis juegos que, para sorpresa de mi amigo, contenía muchas clases pequeñas y varias capas de abstracción. Argumenté que este era el resultado de concentrarme en darle a todo una responsabilidad única y también de aflojar el acoplamiento entre los componentes.
El ECS ha desafiado y cambiado mucho mis puntos de vista. Al igual que usted, solía pensar que la idea misma de la capacidad de mantenimiento es tener la implementación más simple posible para las cosas, lo que implica muchas cosas y, además, muchas cosas interdependientes (incluso si las interdependencias están entre abstracciones). Tiene más sentido si está haciendo zoom en una sola clase o función para querer ver la implementación más sencilla y simple, y si no vemos una, refactorícela e incluso descomponga aún más. Pero puede ser fácil perderse lo que está sucediendo con el mundo exterior como resultado, porque cada vez que divide algo relativamente complejo en 2 o más cosas, esas 2 o más cosas deben interactuar inevitablemente * (ver más abajo) entre sí en algunos camino, o algo afuera tiene que interactuar con todos ellos.
En estos días encuentro que hay un acto de equilibrio entre la simplicidad de algo y cuántas cosas hay y cuánta interacción se requiere. Los sistemas en un ECS tienden a ser bastante pesados con implementaciones no triviales para operar en los datos, como PhysicsSystem
o RenderSystem
o GuiLayoutSystem
. Sin embargo, el hecho de que un producto complejo necesite tan pocos de ellos tiende a facilitar el paso atrás y razonar sobre el comportamiento general de toda la base de código. Hay algo allí que podría sugerir que podría no ser una mala idea apoyarse en menos clases más voluminosas (aún desempeñando una responsabilidad posiblemente discutible), si eso significa menos clases para mantener y razonar, y menos interacciones a lo largo el sistema.
Interacciones
Digo "interacciones" en lugar de "acoplamiento" (aunque reducir interacciones implica reducir ambas), ya que puedes usar abstracciones para desacoplar dos objetos concretos, pero aún se hablan entre sí. Todavía podrían causar efectos secundarios en el proceso de esta comunicación indirecta. Y a menudo encuentro que mi capacidad de razonar sobre la corrección de un sistema está más relacionada con estas "interacciones" que con el "acoplamiento". Minimizar las interacciones tiende a hacerme las cosas mucho más fáciles de razonar sobre todo desde la vista de pájaro. Eso significa que las cosas no se hablan entre sí, y desde ese sentido, ECS también tiende a minimizar realmente las "interacciones", y no solo el acoplamiento, a los mínimos más mínimos (al menos no tengo
Dicho esto, esto podría ser al menos parcialmente yo y mis debilidades personales. He encontrado que el mayor impedimento para crear sistemas de enorme escala, y aún así razonar con confianza sobre ellos, navegar a través de ellos y sentir que puedo hacer cualquier cambio deseado en cualquier lugar de manera predecible, es la gestión del estado y los recursos junto con efectos secundarios. Es el mayor obstáculo que comienza a surgir a medida que paso de decenas de miles de LOC a cientos de miles de LOC a millones de LOC, incluso para el código que escribí completamente por mi cuenta. Si algo me va a retrasar por encima de todo lo demás, es esta sensación de que ya no puedo entender lo que está sucediendo en términos de estado de la aplicación, datos y efectos secundarios. Eso' No es el tiempo robótico que requiere hacer un cambio que me frena tanto como la incapacidad de comprender los impactos completos del cambio si el sistema crece más allá de la capacidad de mi mente para razonar sobre ello. Y reducir las interacciones ha sido, para mí, la forma más efectiva de permitir que el producto crezca mucho más con muchas más características sin que yo personalmente me sienta abrumado por estas cosas, ya que reducir las interacciones al mínimo también reduce la cantidad de lugares que pueden incluso posiblemente cambie el estado de la aplicación y cause efectos secundarios de manera sustancial.
Puede convertirse en algo como esto (donde todo en el diagrama tiene funcionalidad, y obviamente un escenario del mundo real tendría muchas, muchas veces la cantidad de objetos, y este es un diagrama de "interacción", no uno de acoplamiento, como un acoplamiento uno tendría abstracciones en el medio):
... a esto donde solo los sistemas tienen funcionalidad (los componentes azules ahora son solo datos, y ahora este es un diagrama de acoplamiento):
Y están surgiendo pensamientos sobre todo esto y tal vez una forma de enmarcar algunos de estos beneficios en un contexto OOP más conforme que sea más compatible con SOLID, pero todavía no he encontrado los diseños y las palabras, y lo encuentro difícil desde la terminología que estaba acostumbrado a lanzar todo lo relacionado directamente con OOP. Sigo tratando de resolverlo leyendo las respuestas de las personas aquí y también haciendo mi mejor esfuerzo para formular las mías, pero hay algunas cosas muy interesantes sobre la naturaleza de un ECS que no he podido identificar perfectamente. podría ser más ampliamente aplicable incluso a arquitecturas que no lo usan. ¡También espero que esta respuesta no salga como una promoción de ECS! Simplemente me parece muy interesante ya que diseñar un ECS realmente ha cambiado mis pensamientos drásticamente,