Al tratar con SOLID, a menudo me encontré con el hecho de que no seguir estos principios puede conducir a problemas. Los problemas son conocidos, pero poco formalizados. Este artículo está escrito con el objetivo de formalizar situaciones típicas que surgen en el proceso de escribir código, las posibles soluciones y las consecuencias derivadas de esto. Hablaremos sobre por qué enfrentamos códigos incorrectos y cómo crecen los problemas con el crecimiento del programa.
Desafortunadamente, en la mayoría de los casos, la evaluación se reduce a las "muchas" y "una" opciones, lo que insinúa la insolvencia de la notación O, pero incluso dicho análisis permitirá comprender mejor qué código es realmente peligroso para el desarrollo posterior del sistema y qué código es tolerante.
Definición
Decimos que un cambio en el programa requiere "O" de las acciones f (n) si el programador no necesita hacer más que f (n) cambios lógicamente separados en el programa para implementar el cambio con precisión a un factor constante, donde n significa el tamaño del programa.
Métricas
Considere algunas de las características de diseño de Robert Martin y evalúelas en términos de notación O.
Un diseño es difícil si un solo cambio provoca una cascada de otros cambios en los módulos dependientes. Cuantos más módulos tenga que cambiar, más rígido será el diseño.
La diferencia significativa es O (1) y O (n) cambios. Es decir nuestro diseño permite una cantidad constante de cambios o, a medida que el programa crece, la cantidad de cambios aumentará. A continuación, deberíamos considerar los cambios en sí mismos, que también pueden resultar rígidos con algún comportamiento asintótico. Por lo tanto, la rigidez puede ser compleja hasta O (nm). El parámetro m se llamará profundidad de rigidez. Incluso una estimación aproximada de la profundidad de la rigidez en un diseño que incluso permite la rigidez O (n) es demasiado complicada para una persona, ya que cada cambio debe verificarse para detectar cambios aún más profundos.
La fragilidad es propiedad de un programa que se daña en muchos lugares cuando se realiza un solo cambio. A menudo surgen nuevos problemas en partes que no tienen conexión conceptual con la que se cambió.
No consideraremos la cuestión de la conexión lógica de los módulos en los que ocurren los cambios. Por lo tanto, desde el punto de vista de la notación, no hay diferencia entre fragilidad y rigidez, y los argumentos válidos para la rigidez se aplican a la fragilidad.
Un diseño es inerte si contiene partes que podrían ser útiles en otros sistemas, pero los esfuerzos y riesgos asociados con tratar de separar estas partes del sistema original son demasiado grandes.
Los riesgos y esfuerzos en esta definición pueden interpretarse como el número de cambios que ocurren en el módulo cuando se intenta abstraerlo del sistema original como no constante a medida que crece el tamaño del módulo. Sin embargo, como muestra la práctica, todavía vale la pena hacer un resumen, ya que esto ordena el módulo en sí y permite que se transfiera a otros proyectos. Muy a menudo, después de la primera necesidad de transferir el módulo a otro proyecto, aparecen otros similares.
Viscosidad
Ante la necesidad de hacer un cambio, el desarrollador generalmente encuentra varias formas de hacerlo. Algunos conservan el diseño, otros no (es decir, son esencialmente un "hack"). Si los enfoques de conservación del diseño son más difíciles de implementar que un truco, entonces la viscosidad del diseño es alta. Resolver el problema es incorrecto, fácil, pero correcto es difícil. Queremos diseñar nuestros programas para que sea fácil hacer cambios que preserven el diseño.
La siguiente viscosidad puede llamarse miopía en términos de notación O. Sí, al principio, el desarrollador realmente tiene la oportunidad de hacer un cambio en O (1) en lugar de O (n) (debido a la rigidez o fragilidad), pero a menudo tales cambios conducen a una mayor rigidez y fragilidad, es decir, aumentar la profundidad de la rigidez Si ignora dicha "campana", es posible que los cambios posteriores ya no sean posibles de resolver con un "pirateo" y tendrá que hacer cambios en las condiciones de rigidez (quizás más que antes de la "campana") o poner el sistema en buenas condiciones. Es decir, cuando se detecta la viscosidad, es mejor reescribir inmediatamente el sistema correctamente.
Sucede así: Ivan necesita escribir un código que riza su pequeño pie. Al subir a diferentes partes del programa, donde, como sospecha, el bokryad se ha fumado más de una vez, encuentra un fragmento adecuado. Lo copia, lo pega en su módulo y realiza los cambios necesarios.
Pero Ivan no sabe que el código que extrajo con el mouse colocó a Peter allí, tomándolo del módulo escrito por Sveta. Sveta fue la primera en fumar un poco de bordillo, pero sabía que este proceso era muy similar al de fumar marmotas. Encontró en alguna parte un código que karmyachit karmaglot, lo copió en su módulo y lo modificó. Cuando el mismo código aparece en formas ligeramente diferentes una y otra vez, los desarrolladores pierden la idea de la abstracción.
En esta situación, resulta obvio que cuando existe la necesidad de cambiar la base de la acción excavada, este cambio debe hacerse en n lugares. Dada la posibilidad de modificaciones únicas en cada pegado de copia, esto también puede resultar en cambios lógicamente no relacionados. En este caso, existe la posibilidad de olvidarse simplemente del cambio en otro lugar, ya que no hay protección en la etapa de compilación. Por lo tanto, esto también puede convertirse en iteraciones de prueba O (n).
Aplicación sobre notación sólida
SRP (Principio de responsabilidad única). Una entidad de software debe tener solo una razón para el cambio (responsabilidad). En otras palabras, por ejemplo, la clase no debe seguir la lógica de negocios y el mapeo, porque Al cambiar una responsabilidad, debemos asegurarnos de no haber dañado otra responsabilidad. Es decir, la inconsistencia con el principio SRP resulta en rigidez y fragilidad. Seguir este principio también ayuda a eliminar la inercia y transferir módulos de un programa a otro con un número potencialmente menor de dependencias.
El comportamiento asintótico de los cambios sigue siendo básicamente el mismo que sin seguir el principio, pero el factor constante disminuye significativamente. En ambos casos, debemos verificar todo el contenido de la clase y, en caso de cambiar la interfaz de la entidad, los lugares donde interactúan con esta entidad. Solo seguir SRP ayuda a reducir la interfaz y la probabilidad de su cambio, así como la cantidad de implementación interna, que después del cambio puede ser defectuosa. Un razonamiento similar, truncado a la discusión de interfaces, es válido para ISP (Principio de segregación de interfaz).
OCP (principio de cierre abierto). Las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para expansión y cerradas para modificación. Esto debe entenderse de tal manera que podamos modificar el comportamiento del módulo sin afectar su código fuente. Por lo general, esto se logra a través de la herencia y el polimorfismo. Dado que violar el principio LSP es una violación de OCP, y DIP es un medio para mantener OCP, lo siguiente puede aplicarse tanto a LSP como a DIP. El incumplimiento del principio de apertura-cercanía obliga a realizar cambios en todas las entidades que no están cerradas con respecto a este cambio.
Una situación bastante trivial es, por ejemplo, la presencia de una cadena de ifs que determina el tipo de variable entre la lista de clases secundarias. Dichas estructuras pueden aparecer en el programa más de una vez. Y cada vez que agregue una nueva clase secundaria, debe realizar los cambios apropiados en cada cadena. Situaciones similares pueden ocurrir no solo con las clases secundarias, sino también con la consideración de no todos los casos particulares posibles [Esto se refiere a casos no en el momento de la escritura, sino en general. Los casos pueden aparecer más tarde].
Ahora considere la situación cuando hacemos m cambios del mismo tipo, que, debido a la discrepancia con el principio de apertura-cercanía, requieren de nosotros n operaciones. Luego, si dejamos todo como está, apoyando la arquitectura para considerar casos especiales y no generalizamos, obtendremos la complejidad general para todos los cambios O (mn). Si cerramos todos los m lugares con respecto a este cambio, los cambios posteriores tomarán O (1) en lugar de O (m). Por lo tanto, la complejidad general se reduce a O (m + n). Esto significa que comenzar un OCP nunca es demasiado tarde.
Martin dice acerca de esta situación que no debe adivinar (si no está seguro, por supuesto) cómo cerrar desde el primer cambio, pero después del primer cambio vale la pena cerrarlo, ya que el primer cambio fue un marcador de que el sistema no permanecerá necesariamente en el estado actual. Esto es lógico, ya que hacemos acciones O (1 * n) debido a la falta de cercanía, y luego acciones O (m) para cerrarnos de los cambios posteriores. En total, obtenemos la complejidad general O (n + m), pero al mismo tiempo hacemos todas las acciones exactamente cuando se hacen necesarias y no hacemos nada por adelantado, sin saber si será necesario.
Patrones y notación O
Se puede establecer una analogía más entre la notación O en la teoría computacional y la notación O en el diseño. Consiste en el hecho de que reducimos el número de cálculos utilizando algoritmos y estructuras de datos, como árboles de búsqueda y montones, que resuelven problemas típicos más rápido que las soluciones "de frente", y el número de operaciones de un programador con un buen diseño, en el que puede usar también buenas soluciones típicas llamados patrones de diseño. Puede evaluar el efecto de los patrones en el contexto de los principios de SOLID y, como resultado, en el contexto de la notación O.
Por ejemplo, la plantilla Mediator elimina la posibilidad de romper algo en el programa al cambiar la lógica de interacción entre dos objetos, ya que lo encapsula por completo y garantiza la complejidad constante de dicho cambio.
La plantilla Adaptador nos permite usar (leer, agregar) entidades con diferentes interfaces, que usaremos para un propósito común. Con esta plantilla, puede incrustar un nuevo objeto con una interfaz incompatible en el sistema para la cantidad de operaciones que no depende del tamaño del sistema.
Como las estructuras de datos pueden soportar algunas operaciones con buenos asintóticos, y algunas con malas, los patrones se comportan de manera flexible con respecto a algunos cambios y rígidamente con respecto a otros.
Límites razonables
Cuando se trata de la notación O, trabajando en un problema de optimización, debemos recordar que no siempre el algoritmo con las mejores asintóticas es el más adecuado para resolver el problema. Debe entenderse que la clasificación por una burbuja para una matriz de 3 elementos funcionará más rápido que la piramidal, a pesar del hecho de que la clasificación piramidal tiene mejores asintóticos. Para valores pequeños de n, un factor constante juega un papel importante, que la notación O oculta. La notación O en diseño funciona de la misma manera. Para proyectos pequeños, no tiene sentido cercar una gran cantidad de plantillas, ya que los costos de su implementación exceden el número de cambios que se deben hacer desde un "diseño deficiente".