Fuentes de inspiración
Esta publicación surgió gracias a una publicación reciente de
Aras Prantskevichus sobre un informe destinado a programadores junior. Habla sobre cómo adaptarse a las nuevas arquitecturas ECS. Aras sigue el patrón habitual (
explicación a continuación ): muestra ejemplos del terrible código OOP y luego demuestra que el modelo relacional (
pero lo llama "ECS" en lugar de relacional ) es una gran alternativa. De ninguna manera critico a Aras, ¡soy un gran admirador de su trabajo y lo elogio por su excelente presentación! Elegí su presentación en lugar de cientos de otras publicaciones de ECS de Internet porque hizo un esfuerzo adicional y publicó un repositorio git para estudiar en paralelo con la presentación. Contiene un pequeño "juego" simple, usado como un ejemplo de la selección de diferentes soluciones arquitectónicas. Este pequeño proyecto me permitió demostrar mis comentarios sobre un material específico, ¡así que gracias, Aras!
Las diapositivas de Aras están disponibles aquí:
http://aras-p.info/texts/files/2018Academy - ECS-DoD.pdf , y el código está en github:
https://github.com/aras-p/dod-playground .
No analizaré (¿todavía?) Analizar la arquitectura ECS resultante de este informe, pero me centraré en el código de "POO malo" (similar al truco relleno) desde el principio. Mostraré cómo se vería realmente si todas las violaciones de los principios de OOD (diseño orientado a objetos, diseño orientado a objetos) se corrigieran correctamente.
Spoiler: la eliminación de todas las violaciones de OOD conduce a mejoras de rendimiento similares a las conversiones de Aras a ECS, ¡también usa menos RAM y requiere menos líneas de código que la versión ECS!TL; DR: Antes de concluir que OOP es una mierda y unidades ECS, haga una pausa y examine OOD (para saber cómo usar OOP correctamente), y también comprenda el modelo relacional (para saber cómo aplicar correctamente ECS).He participado en muchas discusiones sobre ECS en el foro durante mucho tiempo, en parte porque no creo que este modelo merezca existir como un término separado (
spoiler: esta es solo una versión ad-hoc del modelo relacional ), pero también porque Casi
todas las publicaciones, presentaciones o artículos que promueven un patrón ECS siguen la siguiente estructura:
- Muestre un ejemplo de código OOP terrible, cuya implementación tiene fallas terribles debido al uso excesivo de la herencia (lo que significa que esta implementación viola muchos principios de OOD).
- Para mostrar que la composición es una mejor solución que la herencia (y sin mencionar que OOD realmente nos da la misma lección).
- Demuestre que el modelo relacional es ideal para juegos (pero llámelo "ECS").
Tal estructura me enfurece porque:
(A) este es un truco "relleno" ... que compara lo suave con lo cálido (código incorrecto y buen código) ... y esto es deshonesto, incluso si se hace de manera no intencional y no se requiere que demuestre que la nueva arquitectura es buena; y, lo que es más importante:
(B) tiene un efecto secundario: este enfoque suprime el conocimiento y desanima involuntariamente a los lectores del conocimiento de los estudios realizados durante medio siglo. Comenzaron a escribir sobre el modelo relacional en la década de 1960. A lo largo de los años 70 y 80, este modelo ha mejorado significativamente. Los principiantes a menudo tienen preguntas como "
¿en qué clase quieres poner estos datos? ", Y en respuesta a menudo se les dice algo vago, como "
solo necesitas ganar experiencia y luego aprender a comprender internamente " ... pero en los años 70 esta pregunta fue activa estudió y en el caso general se dedujo una respuesta formal; Esto se llama
normalización de la base de datos . Descartando la investigación existente y llamando a ECS una solución completamente nueva y moderna, ocultas este conocimiento a los principiantes.
¡Los fundamentos de la programación orientada a objetos se establecieron hace mucho tiempo, si no antes (
este estilo comenzó a explorarse en el trabajo de la década de 1950 )! Sin embargo, fue en la década de 1990 que la orientación a objetos se puso de moda, viral y muy rápidamente se convirtió en el paradigma de programación dominante. Se ha producido la explosión de popularidad de muchos nuevos lenguajes OO, incluidos Java y la C ++ (
versión estandarizada ). Sin embargo, dado que esto se debió a la exageración, todos
necesitaban conocer este concepto de alto perfil para escribir en su currículum, pero solo unos pocos realmente entraron en él. Estos nuevos lenguajes crearon las palabras clave -
clase ,
virtual ,
amplia ,
implementa - a partir de muchas características de OO, y creo que es por eso que en ese momento OO se dividió en dos entidades separadas que viven sus propias vidas.
Me referiré al uso de estas características del lenguaje inspiradas en OO como "
OOP " y el uso de técnicas de diseño / arquitectura inspiradas en OO "
OOD ". Todo muy rápidamente recogió la POO. Las instituciones educativas tienen cursos de OO que hornean a nuevos programadores de OOP ... sin embargo, el conocimiento de OOD va a la zaga.
Creo que el código que usa las características del lenguaje de OOP, pero no sigue los principios del diseño OOD,
no es
un código OO . La mayoría de las críticas contra OOP usan código destripado como ejemplo, que no es realmente un código OO.
El código OOP tiene muy mala reputación, y en particular porque la mayoría del código OOP no sigue los principios de OOD y, por lo tanto, no es un código OO "verdadero".
Antecedentes
Como se indicó anteriormente, la década de 1990 se convirtió en el pico de la "moda OO", y fue en ese momento que la "mala OOP" fue probablemente la peor. Si estudiaste POO en ese momento, lo más probable es que hayas aprendido sobre los "cuatro pilares de la POO":
- Abstracción
- Encapsulación
- Polimorfismo
- Herencia
Prefiero llamarlos no cuatro pilares, sino "cuatro herramientas OOP". Estas son herramientas que
puede usar para resolver problemas. Sin embargo, no basta con descubrir cómo funciona la herramienta, debe saber cuándo usarla ... Por parte de los maestros, es irresponsable enseñar a las personas una nueva herramienta, no decirles cuándo vale la pena usar cada una de ellas. A principios de la década de 2000, hubo resistencia al mal uso activo de estas herramientas, una especie de "segunda ola" de pensamiento OOD. El resultado fue la aparición de mnemónicos
SÓLIDOS , que proporcionaron una forma rápida de evaluar las fortalezas arquitectónicas. Cabe señalar que esta sabiduría en realidad se extendió en los años 90, pero aún no ha recibido un acrónimo genial, lo que permitió que se fijaran como cinco principios básicos ...
- El principio de responsabilidad exclusiva ( principio de responsabilidad individual). Cada clase debe tener solo una razón para el cambio. Si la clase "A" tiene dos responsabilidades, entonces debe crear la clase "B" y "C" para procesar cada una de ellas individualmente, y luego crear "A" a partir de "B" y "C".
- El principio de apertura / cierre ( O pen / principio cerrado). El software cambia con el tiempo ( es decir, su soporte es importante ). Intente poner las partes que tienen más probabilidades de cambiar en las implementaciones ( es decir, en clases específicas ) y cree interfaces basadas en esas partes que no es probable que cambien ( por ejemplo, clases base abstractas ).
- El principio de sustitución de Barbara Liskov (principio de sustitución de L iskov). Cada implementación de una interfaz debe cumplir al 100% los requisitos de esta interfaz, es decir cualquier algoritmo que funcione con una interfaz debería funcionar con cualquier implementación.
- El principio de separación de la interfaz ( principio de segregación de interfaz ). Haga que las interfaces sean lo más pequeñas posible para que cada parte del código "sepa" sobre la cantidad más pequeña de código base, por ejemplo, evite dependencias innecesarias. Este consejo también es bueno para C ++, donde los tiempos de compilación se vuelven enormes si no lo sigue.
- El principio de inversión de dependencia ( principio de inversión de dependencia ). En lugar de dos implementaciones específicas que se comunican directamente (y dependen unas de otras), generalmente pueden separarse formalizando su interfaz de comunicación como una tercera clase, utilizada como interfaz entre ellas. Puede ser una clase base abstracta que define las llamadas de los métodos utilizados entre ellos, o incluso simplemente una estructura POD que define los datos transferidos entre ellos.
- Otro principio no está incluido en el acrónimo SOLID, pero estoy seguro de que es muy importante: "Prefiera la composición sobre la herencia" (Principio de reutilización de compuestos). La composición es la opción correcta por defecto . La herencia se debe dejar para los casos en que sea absolutamente necesario.
Entonces obtenemos SOLID-C (++)

A continuación me referiré a estos principios, llamándolos acrónimos: SRP, OCP, LSP, ISP, DIP, CRP ...
Algunas notas más:
- En OOD, los conceptos de interfaces e implementaciones no pueden vincularse a ninguna palabra clave específica de OOP. En C ++, a menudo creamos interfaces con clases base abstractas y funciones virtuales , y luego las implementaciones heredan de estas clases base ... pero esta es solo una forma específica de implementar el principio de la interfaz. En C ++, también podemos usar PIMPL , punteros opacos , tipeo de pato , typedef, etc. ¡Puede crear una estructura OOD y luego implementarla en C, en la que no hay palabras clave del lenguaje OOP! Entonces, cuando hablo de interfaces , no me refiero necesariamente a las funciones virtuales , estoy hablando del principio de ocultar la implementación . Las interfaces pueden ser polimórficas , ¡pero la mayoría de las veces lo son! El polimorfismo rara vez se usa correctamente, pero las interfaces son un concepto fundamental para todo el software.
- Como dejé claro anteriormente, si crea una estructura POD que simplemente almacena algunos datos para la transmisión de una clase a otra, entonces esta estructura se usa como una interfaz ; esta es una descripción formal de los datos .
- Incluso si solo crea una clase separada con las partes pública y privada , entonces todo lo que está en la parte común es una interfaz , y todo en la parte privada es una implementación .
- La herencia en realidad tiene (al menos) dos tipos: herencia de interfaz y herencia de implementación.
- En C ++, la herencia de interfaz incluye clases base abstractas con funciones virtuales puras, PIMPL, typedef condicional. En Java, la herencia de la interfaz se expresa a través de la palabra clave implements .
- En C ++, la herencia de implementaciones ocurre cada vez que las clases base contienen algo más que funciones virtuales puras. En Java, la herencia de implementación se expresa utilizando la palabra clave extend .
- ¡OOD tiene muchas reglas para heredar interfaces, pero vale la pena considerar la herencia de implementaciones como "código con un mordisco" !
Y finalmente, debo mostrar algunos ejemplos de la terrible capacitación de OOP y cómo conduce a un mal código en la vida real (y la mala reputación de OOP).
- Cuando le enseñaron jerarquías / herencia, es posible que le hayan asignado una tarea similar: suponga que tiene una aplicación universitaria que contiene un directorio de estudiantes y personal. Puede crear la clase base Persona, y luego la clase Estudiante y la clase Personal, heredadas de Persona.
No no no Aquí te detendré. La implicación tácita del principio LSP es que las jerarquías de clase y los algoritmos que las procesan son simbióticos. Estas son las dos mitades de todo el programa. OOP es una extensión de la programación de procedimientos, y todavía se asocia principalmente con estos procedimientos. Si no sabemos qué tipos de algoritmos funcionarán con los estudiantes y el personal ( y qué algoritmos se simplificarán debido al polimorfismo ), entonces será completamente irresponsable comenzar a crear la estructura de las jerarquías de clases. Primero necesita conocer los algoritmos y los datos. - Cuando le enseñaron jerarquías / herencia, probablemente le dieron una tarea similar: suponga que tiene una clase de formas. También tenemos cuadrados y rectángulos como subclases. ¿Debería un cuadrado ser un rectángulo o un rectángulo un cuadrado?
Este es realmente un buen ejemplo para demostrar la diferencia entre la herencia de implementaciones y la herencia de interfaces.
TL; DR: su clase de OOP le dijo cómo era la herencia. ¡Tu clase de OOD faltante debería haberte dicho que no la uses el 99% del tiempo!
Conceptos de entidad / componente
Habiendo tratado con los requisitos previos, pasemos a donde comenzó Aras, al llamado punto de partida de una "OOP típica".
Pero para empezar, una adición más: Aras llama a este código "OOP tradicional", y quiero objetarlo. Este código puede ser típico para OOP en el mundo real, pero, al igual que los ejemplos anteriores, viola todo tipo de principios básicos de OO, por lo que no debe considerarse como tradicional.
Comenzaré con el primer commit antes de que él comenzara a rehacer la estructura hacia ECS:
“Haz que funcione nuevamente en Windows” 3529f232510c95f53112bbfff87df6bbc6aa1fae
Sí, es difícil descifrar cien líneas de código de inmediato, así que comencemos gradualmente ... Necesitamos otro aspecto de los requisitos previos: era popular usar la herencia en los juegos de los años 90 para resolver todos los problemas de reutilización de código. Tenías Entity, Character extensible, Player y Monster extensibles, y así sucesivamente ... Esta es una herencia de implementaciones, como describimos anteriormente (
"código con estrangulador" ), y parece que es correcto comenzar con eso, pero como resultado conduce a base de código inflexible. Porque OOD tiene el principio de "composición sobre herencia" descrito anteriormente. Entonces, en la década de 2000, el principio de "composición sobre herencia" se hizo popular, y los desarrolladores de juegos comenzaron a escribir código similar.
¿Qué hace este código? Bueno no bueno

En resumen,
este código vuelve a implementar una característica existente del lenguaje: la composición como una biblioteca de tiempo de ejecución y no como una característica del lenguaje. Puede imaginar esto como si el código estuviera realmente creando un nuevo metalenguaje sobre C ++ y una máquina virtual (VM) para ejecutar este metalenguaje. En el juego de demostración de Aras, este código no es obligatorio (
¡lo eliminaremos por completo pronto! ) Y solo sirve para reducir el rendimiento del juego unas 10 veces.
¿Pero qué hace él realmente? Este es el concepto de "sistema de componente
E / entidad" (a
veces por alguna razón llamado "sistema de componente E / C " ), pero es completamente diferente del concepto de "sistema
C sistema de componente omponente "(" sistema de componente de entidad ") (
que por razones obvias nunca se llama" sistemas de sistema de componente de entidad de la entidad). Formaliza varios principios de la "CE":
- el juego se construirá a partir de no tener características de "Entidades" ("Entidad") ( en este ejemplo, llamadas GameObjects), que consisten en "componentes" ("Componente").
- GameObjects implementa el patrón de "localizador de servicios" : sus componentes secundarios se consultarán por tipo.
- Los componentes saben a qué GameObject pertenecen: pueden encontrar componentes que estén en el mismo nivel con ellos consultando el GameObject padre.
- La composición puede tener solo un nivel de profundidad (los componentes no pueden tener sus propios componentes secundarios, GameObjects no puede tener GameObjects secundarios ).
- GameObject puede tener solo un componente de cada tipo ( en algunos marcos esto es un requisito obligatorio, en otros no ).
- Cada componente (probablemente) cambia con el tiempo de una manera no especificada, por lo que la interfaz contiene una "Actualización virtual vacía".
- Los GameObjects pertenecen a una escena que puede ejecutar consultas en todos los GameObjects (y, por lo tanto, en todos los componentes).
Un concepto similar fue muy popular en la década de 2000, y a pesar de sus limitaciones, resultó ser lo suficientemente flexible como para crear innumerables juegos tanto en la actualidad como en la actualidad.
Sin embargo, esto no es obligatorio. Su lenguaje de programación ya tiene soporte para la composición como una característica del lenguaje: no hay necesidad de un concepto hinchado para acceder a él ... ¿Por qué, entonces, existen estos conceptos? Bueno, para ser sincero, te permiten realizar
composiciones dinámicas en tiempo de ejecución . En lugar de definir tipos de GameObject en el código, puede cargarlos desde archivos de datos. Y esto es muy conveniente, porque permite a los diseñadores de juegos / niveles crear sus propios tipos de objetos ... Sin embargo, en la mayoría de los proyectos de juegos hay muy pocos diseñadores y literalmente un ejército completo de programadores, por lo que diría que esta es una oportunidad importante. Peor aún, ¡esta no es la única forma en que puede implementar una composición en tiempo de ejecución! Por ejemplo, Unity usa C # como su "lenguaje de programación", y muchos otros juegos usan sus alternativas, por ejemplo, Lua, una herramienta conveniente para los diseñadores que puede generar código C # / Lua para definir nuevos objetos de juego sin la necesidad de un concepto tan inflado. Volveremos a agregar esta "característica" en la próxima publicación y haremos que no nos cueste una disminución de diez veces en el rendimiento ...
Evaluemos este código de acuerdo con OOD:
- GameObject :: GetComponent usa dynamic_cast. La mayoría de la gente te dirá que dynamic_cast es un "código con un estrangulador", una gran pista de que tienes un error en alguna parte. Diría esto, esto es evidencia de que violó el LSP , tiene algún tipo de algoritmo que funciona con la interfaz base, pero necesita conocer diferentes detalles de implementación. Por esta razón en particular, el código huele mal.
- GameObject, en principio, no es malo, si imagina que implementa la plantilla de "localizador de servicios" ... pero si va más allá de las críticas desde el punto de vista de OOD, esta plantilla crea conexiones implícitas entre partes del proyecto, y creo que ( sin un enlace a Wikipedia que pueda soportar yo con conocimiento de la informática ) que los canales de comunicación implícitos son un antipatrón , y deberían preferir canales de comunicación explícitos. El mismo argumento se aplica al hinchado "concepto de eventos" que a veces se usa en los juegos ...
- Quiero afirmar que un componente es una violación de SRP porque su interfaz ( virtual void Update (time) ) es demasiado amplia. El uso de la "Actualización virtual de vacío" en el desarrollo de juegos es omnipresente, pero también diría que es antipattern. Un buen software debería permitirle pensar fácilmente sobre el flujo de control y el flujo de datos. Colocar cada elemento del código de juego detrás de la llamada "Actualización virtual vacía" ofusca completamente y completamente el flujo de control y el flujo de datos. En mi humilde opinión, los efectos secundarios invisibles, también llamados de largo alcance, son algunas de las fuentes más comunes de errores, y la "Actualización de vacío virtual" asegura que casi todo será un efecto secundario invisible.
- Aunque el objetivo de la clase Component es habilitar la composición, lo hace a través de la herencia, lo cual es una violación de la CRP .
- El único lado bueno de este ejemplo es que el código del juego es excesivo para cumplir con los principios de SRP e ISP: está dividido en muchos componentes simples con muy poca responsabilidad, lo cual es excelente para reutilizar el código.
Sin embargo, no es tan bueno para mantener DIP: muchos componentes tienen conocimiento directo el uno del otro.
Por lo tanto, todo el código que se muestra arriba se puede eliminar. Toda esta estructura. Eliminar GameObject (también llamado Entidad en otros marcos), eliminar Componente, eliminar FindOfType. Esto es parte de una máquina virtual inútil que viola los principios de OOD y ralentiza terriblemente nuestro juego.
Composición sin marcos (es decir, utilizando características del lenguaje de programación en sí)
Si eliminamos el marco de composición y no tenemos la clase base Componente, ¿cómo lograrán nuestros GameObjects utilizar la composición y consistir en componentes? Como dice el título, en lugar de escribir esta máquina virtual hinchada y crear GameObjects en un extraño metalenguaje, solo escribámoslos en C ++ porque somos programadores de juegos y este es literalmente nuestro trabajo.
Aquí está el commit que eliminó el marco Entity / Component:
https://github.com/hodgman/dod-playground/commit/f42290d0217d700dea2ed002f2f3b1dc45e8c27cAquí está la versión original del código fuente:
https://github.com/hodgman/dod-playground/blob/3529f232510c95f53112bbfff87df6bbc6aa1fae/source/game.cppAquí está la versión modificada del código fuente:
https://github.com/hodgman/dod-playground/blob/f42290d0217d700dea2ed002f2f3b1dc45e8c27c/source/game.cppBrevemente sobre los cambios:
- Se eliminó ": Componente público" de cada tipo de componente.
- Se agregó un constructor a cada tipo de componente.
- OOD se trata principalmente de encapsular el estado de una clase, pero dado que estas clases son tan pequeñas / simples, no hay nada especial que ocultar: una interfaz es una descripción de los datos. Sin embargo, una de las razones principales por las que la encapsulación es el pilar principal es que nos permite garantizar la verdad constante de los invariantes de clase ... o si el invariante está roto, entonces solo necesita examinar el código de implementación encapsulado para encontrar el error. En este ejemplo de código, vale la pena agregar constructores para implementar una invariante simple: todos los valores deben inicializarse.
- Cambié el nombre de los métodos "Actualizar" demasiado generales para que sus nombres reflejen lo que realmente hacen: UpdatePosition para MoveComponent y ResolveCollisions for AvoidComponent.
- Eliminé tres bloques de código codificados que se parecían a una plantilla / prefabricado, el código que crea un GameObject que contiene tipos específicos de Componente, y lo reemplacé con tres clases de C ++.
- Se eliminó la "Actualización de vacío virtual" antipatrón.
- En lugar de que los componentes se busquen entre sí a través de la plantilla de "localizador de servicios", el juego los une explícitamente durante la construcción.
Los objetos
Por lo tanto, en lugar de este código de "máquina virtual":
Ahora tenemos código C ++ normal:
struct RegularObject { PositionComponent pos; SpriteComponent sprite; MoveComponent move; AvoidComponent avoid; RegularObject(const WorldBoundsComponent& bounds) : move(0.5f, 0.7f)
Algoritmos
Se ha realizado otro cambio importante en los algoritmos. ¿Recuerdas que al principio dije que las interfaces y los algoritmos funcionan en simbiosis y deberían influir en la estructura de cada uno? Entonces, la "
Actualización de vacío virtual " antipatrón se ha convertido en el enemigo aquí también. El código inicial contiene el algoritmo del bucle principal, que consiste solo en esto:
Puedes argumentar que es hermoso y simple, pero en mi humilde opinión es muy, muy malo. Esto ofusca completamente tanto el
flujo de control como el
flujo de datos dentro del juego. Si queremos poder comprender nuestro software, si queremos admitirlo, si queremos agregarle cosas nuevas, optimizarlo, ejecutarlo de manera eficiente en varios núcleos de procesador, entonces debemos comprender tanto el flujo de control como el flujo de datos. Por lo tanto, la "Actualización de vacío virtual" debe ser incendiada.
En cambio, creamos un bucle principal más explícito, que simplifica enormemente la comprensión del flujo de control (el
flujo de datos aún está ofuscado, pero lo arreglaremos en las siguientes confirmaciones ).
La desventaja de este estilo es que para
cada nuevo tipo de objeto agregado al juego, tenemos que agregar varias líneas al bucle principal. Volveré sobre esto en una publicación posterior de esta serie.
Rendimiento
Hay muchas violaciones enormes de OOD, se toman algunas malas decisiones al elegir una estructura y hay muchas oportunidades para la optimización, pero las abordaré en la próxima publicación de la serie. Sin embargo, ya en esta etapa está claro que la versión con "OOD fijo" coincide casi por completo o gana el código final "ECS" desde el final de la presentación ... Y todo lo que hicimos fue tomar el código pseudo-OOP incorrecto y hacer que cumpliera con los principios ¡OOP (y también borró cien líneas de código)!
Próximos pasos
Aquí quiero considerar una gama mucho más amplia de problemas, incluida la resolución de los problemas OOD restantes, los objetos inmutables (
programación en un estilo funcional ) y las ventajas que pueden aportar en las discusiones sobre flujos de datos, paso de mensajes, aplicación de lógica DOD a nuestro código OOD, aplicando sabiduría relevante en el código OOD, eliminando estas clases de "entidades" con las que terminamos, y usando solo componentes puros, usando diferentes estilos para conectar componentes (comparando punteros y la responsabilidad de llevar) componentes de contenedores del mundo real, la versión ECS-revisión para una mejor optimización, así como una mayor optimización, no se menciona en el informe de Aras
(tales como multi-threading / SIMD). El orden no será necesariamente este, y quizás no consideraré todo lo anterior ...
Además
Los enlaces al artículo se han extendido más allá de los círculos de desarrolladores de juegos, por lo que agregaré: "
ECS " (
este artículo de Wikipedia es malo, por cierto, combina los conceptos de EC y ECS, y esto no es lo mismo ... ) - esta es una plantilla falsa que circula dentro de las comunidades desarrolladores de juegos. De hecho, es una versión del modelo relacional en el que las "entidades" son solo ID que designan un objeto sin forma, los "componentes" son filas en tablas específicas que hacen referencia a los ID, y los "sistemas" son códigos de procedimiento que pueden modificar componentes. . Esta "plantilla" siempre se ha posicionado como una solución al problema del uso excesivo de la herencia, pero no se menciona que el uso excesivo de la herencia realmente viola las recomendaciones de la OOP. De ahí mi indignación. Esta no es la "única forma verdadera" de escribir software. La publicación está diseñada para garantizar que las personas realmente aprendan sobre los principios de diseño existentes.