Recientemente tuve una discusión con un amigo sobre OOP en el desarrollo de videojuegos.
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.
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. Procedimos a discutir esto durante lo que parecieron siglos, y finalmente acordamos estar en desacuerdo, diciendo que tal vez hubo casos en los que los principios SÓLIDOS y la POO adecuada en realidad no se mezclaron bien.
Incluso la entrada de Wikipedia sobre principios SÓLIDOS establece que son pautas que ayudan a escribir código mantenible y que son parte de una estrategia general de programación ágil y adaptativa.
Entonces, mi pregunta es:
¿Hay casos en OOP donde algunos o todos los principios SOLIDOS no se prestan para limpiar el código?
Puedo imaginar de inmediato que el Principio de sustitución de Liskov podría entrar en conflicto con otro sabor de herencia segura. Es decir, si alguien ideó otro patrón útil implementado a través de la herencia, es muy posible que el LSP esté en conflicto directo con él.
¿Hay otros? ¿Quizás ciertos tipos de proyectos o ciertas plataformas objetivo funcionan mejor con un enfoque menos SÓLIDO?
Editar:
Solo me gustaría especificar que no estoy preguntando cómo mejorar mi código;) La única razón por la que mencioné un proyecto en esta pregunta fue para dar un poco de contexto. Mi pregunta es sobre OOP y principios de diseño en general .
Si tienes curiosidad sobre mi proyecto, mira esto .
Edición 2:
Me imaginé que esta pregunta sería respondida de una de las 3 maneras siguientes:
- 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, SOLID es las rodillas de la abeja y OOP siempre será mejor con ella. Pero, como con todo, no es una panacea. Bebe responsablemente.
Las opciones 1 y 2 probablemente habrían generado respuestas largas e interesantes. La opción 3, por otro lado, sería una respuesta corta, poco interesante, pero en general tranquilizadora.
Parece que estamos convergiendo en la opción 3.
fuente
Respuestas:
En general, no. La historia ha demostrado que los principios SÓLIDOS contribuyen en gran medida a un mayor desacoplamiento, lo que a su vez ha demostrado que aumenta la flexibilidad en el código y, por lo tanto, su capacidad para adaptarse al cambio y hacer que el código sea más fácil de razonar, probar y reutilizar. En resumen, haga su código más limpio.
Ahora, puede haber casos en los que los principios SÓLIDOS colisionen con DRY (no se repita), KISS (manténgalo simple estúpido) u otros principios de buen diseño OO. Y, por supuesto, pueden colisionar con la realidad de los requisitos, las limitaciones de los humanos, las limitaciones de nuestros lenguajes de programación, otros obstáculos.
En resumen, los principios SOLID siempre se prestarán para limpiar el código, pero en algunos escenarios se prestarán menos que alternativas conflictivas. Siempre son buenos, pero a veces otras cosas son más buenas.
fuente
Count
yasImmutable
, y propiedades comogetAbilities
[cuyo retorno indicaría si cosas comoCount
serán "eficientes"], entonces uno podría tener un método estático que tome múltiples secuencias y las agregue para que Se comportará como una sola secuencia más larga. Si tales habilidades están presentes en el tipo de secuencia básica, incluso si solo se encadenan a implementaciones predeterminadas, entonces un agregado podrá exponer esas habilidades ...Creo que tengo una perspectiva inusual en el sentido de que he trabajado tanto en finanzas como en juegos.
Muchos de los programadores que conocí en juegos eran terribles en Ingeniería de Software, pero no necesitaban prácticas como SOLID. Tradicionalmente, ponen su juego en una caja, luego terminan.
En finanzas, descubrí que los desarrolladores pueden ser realmente descuidados e indisciplinados porque no necesitan el rendimiento que tú haces en los juegos.
Ambas declaraciones anteriores son, por supuesto, generalizaciones excesivas, pero en realidad, en ambos casos, el código limpio es crítico. Para la optimización, es esencial un código claro y fácil de entender. Por el bien de la mantenibilidad, quieres lo mismo.
Eso no quiere decir que SOLID no esté exento de críticas. Se llaman principios y, sin embargo, se supone que deben seguirse como directrices. Entonces, ¿cuándo exactamente debo seguirlos? ¿Cuándo debo romper las reglas?
Probablemente podría mirar dos piezas de código y decir cuál cumple mejor con SOLID. Pero de forma aislada, no hay una forma objetiva de transformar el código para que sea SÓLIDO. Definitivamente es a la interpretación del desarrollador.
No es sequitur decir que "un gran número de clases puede llevar a una pesadilla de mantenimiento". Sin embargo, si SRP no se interpreta correctamente, podría terminar fácilmente con este problema.
He visto código con muchas capas de casi ninguna responsabilidad. Las clases que no hacen nada aparte de pasar el estado de una clase a otra. El problema clásico de demasiadas capas de indirección.
SRP si se abusa puede terminar con una falta de cohesión en su código. Puede decir esto cuando intenta agregar una función. Si siempre tiene que cambiar varios lugares al mismo tiempo, su código carece de cohesión.
Open-Closed no está exento de críticas. Por ejemplo, vea la revisión de Jon Skeet del principio de Open Closed . No reproduciré sus argumentos aquí.
Su argumento sobre LSP parece bastante hipotético y realmente no entiendo lo que quiere decir con otra forma de herencia segura.
ISP parece duplicar algo SRP. Seguramente deben interpretarse de la misma manera que nunca se puede anticipar lo que harán todos los clientes de su interfaz. La interfaz cohesiva de hoy es el violador de ISP de mañana. La única conclusión ilógica es no tener más de un miembro por interfaz.
DIP puede conducir a un código inutilizable. He visto clases con una gran cantidad de parámetros en nombre de DIP. Estas clases normalmente habrían "renovado" sus partes compuestas, pero en nombre de la capacidad de prueba, nunca debemos volver a usar la nueva palabra clave.
SOLID por sí solo no es suficiente para escribir código limpio. He visto el libro de Robert C. Martin, " Código limpio: un manual de artesanía ágil de software ". El mayor problema es que es fácil leer este libro e interpretarlo como reglas. Si haces eso, te estás perdiendo el punto. Contiene una gran lista de olores, heurística y principios, muchos más que solo los cinco de SOLID. Todos estos "principios" no son realmente principios sino pautas. El tío Bob los describe a sí mismo como "agujeros de cubículo" para los conceptos. Son un acto de equilibrio; seguir cualquier directriz a ciegas causará problemas.
fuente
Aquí está mi opinión:
Aunque los principios SOLID apuntan a una base de código flexible y no redundante, esto podría ser una compensación en la legibilidad y el mantenimiento si hay demasiadas clases y capas.
Se trata especialmente de la cantidad de abstracciones . Un gran número de clases podría estar bien si se basan en pocas abstracciones. Dado que no conocemos el tamaño y las características específicas de su proyecto, es difícil saberlo, pero es un poco preocupante que mencione varias capas además de una gran cantidad de clases.
Supongo que los principios SOLID no están en desacuerdo con OOP, pero aún es posible escribir código no limpio que se adhiera a ellos.
fuente
En mis primeros días de desarrollo, comencé a escribir un Roguelike en C ++. Como quería aplicar la buena metodología orientada a objetos que aprendí de mi educación, inmediatamente vi los elementos del juego como objetos de C ++.
Potion
,Swords
,Food
,Axe
,Javelins
Etc, todo derivado de un núcleo básicoItem
de código, nombre, icono, peso, ese tipo de cosas manipulación.Luego, codifiqué la lógica de la bolsa y enfrenté un problema. Puedo poner
Items
en la bolsa, pero luego, si elijo una, ¿cómo sé si es unaPotion
o unaSword
? Busqué en Internet cómo rechazar artículos. Pirateé las opciones del compilador para habilitar la información de tiempo de ejecución para saber cuáles son misItem
tipos verdaderos abstraídos , y comencé a cambiar la lógica de los tipos para saber qué operaciones permitiría (por ejemplo, evitar beberSwords
y ponerme dePotions
pie)Básicamente, convirtió el código en una gran bola de lodo medio copiado. A medida que avanzaba el proyecto, comencé a comprender cuál era el fallo del diseño. Lo que sucedió es que intenté usar clases para representar valores. Mi jerarquía de clases no era una abstracción significativa. Lo que debería haber hecho es hacer
Item
poner en práctica un conjunto de funciones, como por ejemploEquip ()
,Apply ()
yThrow ()
, y hacer que cada uno se comporte basado en valores. Usar enumeraciones para representar máquinas tragamonedas. Usar enumeraciones para representar diferentes tipos de armas. Usando más valores y menos clasificación, porque mi subclasificación no tenía otro propósito que llenar estos valores finales.Como se enfrenta a la multiplicación de objetos bajo una capa de abstracción, creo que mi visión puede ser valiosa aquí. Si parece tener demasiados tipos de objetos, es posible que esté confundiendo lo que deberían ser tipos con lo que deberían ser valores.
fuente
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.
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).
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
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
oRenderSystem
oGuiLayoutSystem
. 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,
fuente
Los principios SÓLIDOS son buenos, pero KISS y YAGNI tienen prioridad en caso de conflictos. El propósito de SOLID es administrar la complejidad, pero si la aplicación de SOLID en sí hace que el código sea más complejo, se pierde el propósito. Por poner un ejemplo, si tiene un programa pequeño con solo unas pocas clases, la aplicación de DI, la segregación de interfaz, etc. podría aumentar la complejidad general del programa.
El principio abierto / cerrado es especialmente controvertido. Básicamente dice que debe agregar características a una clase creando una subclase o una implementación separada de la misma interfaz, pero sin tocar la clase original. Si mantiene una biblioteca o un servicio utilizado por clientes no coordinados, esto tiene sentido, ya que no desea interrumpir el comportamiento en el que puede confiar el cliente existente. Sin embargo, si está modificando una clase que solo se usa en su propia aplicación, a menudo sería mucho más simple y limpio simplemente modificar la clase.
fuente
En mi experiencia:-
Las clases grandes son exponencialmente más difíciles de mantener a medida que se hacen más grandes.
Las clases pequeñas son más fáciles de mantener y la dificultad de mantener una colección de clases pequeñas aumenta aritméticamente al número de clases.
A veces las clases grandes son inevitables, un gran problema a veces necesita una clase grande, pero son mucho más difíciles de leer y comprender. Para clases más pequeñas bien nombradas, solo el nombre puede ser suficiente para la comprensión, ni siquiera necesita mirar el código a menos que haya un problema muy específico con la clase.
fuente
Siempre me resulta difícil trazar la línea entre la 'S' del sólido y la (puede ser anticuada) la necesidad de dejar que un objeto tenga todo el conocimiento de sí mismo.
Hace 15 años trabajé con desarrolladores de Deplhi que tenían su santo grial para tener clases que contenían todo. Entonces, para una hipoteca, puede agregar seguros e ingresos, préstamos e ingresos e información sobre impuestos, y puede calcular el impuesto a pagar, el impuesto a deducir y puede serializarse, persistir en el disco, etc., etc.
Y ahora tengo el mismo objeto dividido en muchísimas clases más pequeñas que tienen la ventaja de que pueden probarse de forma mucho más fácil y mejor, pero no ofrecen una buena visión general de todo el modelo comercial.
Y sí, sé que no desea vincular sus objetos a la base de datos, pero puede inyectar un repositorio en la gran clase de hipotecas para resolver eso.
Además, no siempre es fácil encontrar buenos nombres para las clases que solo hacen una pequeña cosa.
Así que no estoy seguro de que la 'S' sea siempre una buena idea.
fuente