Advertisement
  1. Code
  2. Designing

Cómo escribir código que adopte el cambio

by
Read Time:20 minsLanguages:

Spanish (Español) translation by Steven (you can also view the original English article)

Escribir código, que es fácil de cambiar es el Santo Grial de la programación. ¡Bienvenido a la programación del nirvana! Pero las cosas son mucho más difíciles en realidad: el código fuente es difícil de entender, las dependencias apuntan en innumerables direcciones, el acoplamiento es molesto y pronto se siente el calor de la programación del infierno. En este tutorial, discutiremos algunos principios, técnicas e ideas que te ayudarán a escribir código que sea fácil de cambiar.


Algunos conceptos orientados a objetos

La programación orientada a objetos (OOP) se hizo popular, debido a su promesa de organización y reutilización de códigos; fracasó completamente en este empeño. Hemos estado usando conceptos de POO durante muchos años, pero seguimos implementando repetidamente la misma lógica en nuestros proyectos. OOP introdujo un conjunto de buenos principios básicos que, si se usan correctamente, pueden llevar a un código mejor y más limpio.

Cohesión

Las cosas que pertenecen juntas deben mantenerse juntas; de lo contrario, deberían ser movidos a otro lugar. A esto se refiere el término cohesión. El mejor ejemplo de cohesión se puede demostrar con una clase:

Este ejemplo define una clase con campos que representan números y tamaños. Estas propiedades, juzgadas solo por sus nombres, no pertenecen juntas. Luego tenemos dos métodos, add() y substract(), que operan solo en las dos variables numéricas. Además, tenemos un método area(), que opera en los campos de longitud y ancho.

Es obvio que esta clase es responsable de grupos separados de información. Tiene muy baja cohesión. Vamos a refactorizarlo.

Esta es una clase altamente cohesiva. ¿Por qué? Porque cada sección de esta clase pertenece una con la otra. Debes luchar por la cohesión, pero ten cuidado, puede ser difícil de lograr.

Ortogonalidad

En términos simples, la ortogonalidad se refiere al aislamiento o eliminación de los efectos secundarios. Un método, clase o módulo que cambia el estado de otras clases o módulos no relacionados no es ortogonal. Por ejemplo, la caja negra de un avión es ortogonal. Tiene su funcionalidad interna, fuente de energía interna, micrófonos y sensores. No tiene efecto en el avión en el que reside, o en el mundo exterior. Solo proporciona un mecanismo para registrar y recuperar datos de vuelo.

Un ejemplo de uno de estos sistemas no ortogonales es la electrónica de tu automóvil. El aumento de la velocidad de tu vehículo tiene varios efectos secundarios, como el aumento del volumen de la radio (entre otras cosas). La velocidad no es ortogonal al coche.

En este ejemplo, el método add() de la clase Calculator muestra un comportamiento inesperado: crea un objeto AlertMechanism y llama a uno de sus métodos. Este es un comportamiento inesperado y no deseado; los consumidores de la biblioteca nunca esperarán un mensaje impreso en la pantalla. En su lugar, solo esperan la suma de los números proporcionados.

Este es mejor. AlertMechanism no tiene efecto en la Calculadora. En su lugar, AlertMechanism utiliza lo que necesita para determinar si se debe emitir una alerta.

Dependencia y Acoplamiento

En la mayoría de los casos, estas dos palabras son intercambiables; pero, en algunos casos, un término es preferido sobre otro.

Entonces, ¿qué es una dependencia? Cuando el objeto A necesita usar el objeto B para realizar su comportamiento prescrito, decimos que A depende de B. En la POO, las dependencias son extremadamente comunes. Los objetos frecuentemente trabajan y dependen unos de otros. Entonces, mientras que eliminar la dependencia es una búsqueda noble, es casi imposible hacerlo. Sin embargo, es preferible controlar las dependencias y reducirlas.

Los términos, acoplamiento pesado y acoplamiento suelto, generalmente se refieren a cuánto depende un objeto de otros objetos.

En un sistema débilmente acoplado, los cambios en un objeto tienen un efecto reducido en los otros objetos que dependen de él. En tales sistemas, las clases dependen de interfaces en lugar de implementaciones concretas (hablaremos más sobre esto más adelante). Esta es la razón por la que los sistemas de acoplamiento flexible están más abiertos a modificaciones.

Acoplamiento en un campo

Consideremos un ejemplo:

Es común ver este tipo de código. Una clase, Display en este caso, depende de la clase Calculator al hacer referencia directamente a esa clase. En el código anterior, el campo $calculator de Display es de tipo Calculator. El objeto que contiene el campo es el resultado de llamar directamente al constructor de Calculator.

Acoplamiento accediendo a los otros métodos de clase

Revisa el siguiente código para ver una demostración de este tipo de acoplamiento:

La clase Display llama al método add() del objeto Calculator. Esta es otra forma de acoplamiento, porque una clase accede al método de la otra.

Acoplamiento por referencia de método

También puedes juntar clases con referencias de métodos. Por ejemplo:

Es importante tener en cuenta que el método makeCalculator() devuelve un objeto Calculator. Esta es una dependencia.

Acoplamiento por polimorfismo

La herencia es probablemente la forma más fuerte de dependencia:

AdvancedCalculator no solo no puede hacer su trabajo sin Calculator, sino que tampoco podría existir sin ella.

Reducción de acoplamiento por inyección de dependencia

Se puede reducir el acoplamiento inyectando una dependencia. Aquí hay un ejemplo:

Al inyectar el objeto Calculator a través del constructor Display, redujimos la dependencia Display en la clase Calculator. Pero esto es solo la mitad de la solución.

Reducción de acoplamiento con interfaces

Podemos reducir aún más el acoplamiento mediante el uso de interfaces. Por ejemplo:

Puedes pensar en ISP como un principio de cohesión de nivel superior.

Este código introduce la interfaz CanCompute. Una interfaz es tan abstracta como se puede obtener en OOP; define los miembros que una clase debe implementar. En el caso del ejemplo anterior, Calculator implementa la interfaz CanCompute.

El constructor Display espera un objeto que implemente CanCompute. En este punto, la dependencia Display con Calculator se rompe efectivamente. En cualquier momento, podemos crear otra clase que implemente CanCompute y pasar un objeto de esa clase al constructor Display. Ahora Display depende solo de la interfaz CanCompute, pero incluso esa dependencia es opcional. Si no pasamos ningún argumento al constructor Display, simplemente creará un objeto Calculator clásico llamando a makeCalculator(). Esta técnica se usa con frecuencia y es extremadamente útil para el desarrollo guiado por pruebas (TDD).


Los Principios de SOLID

SOLID es un conjunto de principios para escribir código limpio, que luego facilita el cambio, el mantenimiento y la extensión en el futuro. Son recomendaciones que, cuando se aplican al código fuente, tienen un efecto positivo en la mantenibilidad.

Una pequeña historia

Los principios SOLID, también conocidos como principios ágiles, fueron definidos inicialmente por Robert C. Martin. A pesar de que no inventó todos estos principios, fue él quien los juntó. Puedes leer más sobre ellos en su libro: Desarrollo de software ágil, principios, patrones y prácticas. Los principios de SOLID cubren una amplia gama de temas, pero los presentaré de la manera más simple posible. No dudes en pedir detalles adicionales en los comentarios, si es necesario.

Principio de Responsabilidad Única (SRP)

Una clase tiene una sola responsabilidad. Esto puede parecer simple, pero a veces puede ser difícil de entender y poner en práctica.

¿Quién crees que se beneficia con el comportamiento de esta clase? Bueno, un departamento de contabilidad es una opción (para el saldo), el departamento de finanzas puede ser otra (para los informes de ingresos/pagos), e incluso el departamento de archivo podría imprimir y archivar los informes.

Hay cuatro razones por las que podrías tener que cambiar esta clase; cada departamento puede querer personalizar sus respectivos métodos para sus necesidades.

El SRP recomienda dividir estas clases en clases más pequeñas y específicas para el comportamiento, cada una con una sola razón para cambiar. Tales clases tienden a ser altamente cohesivas y débilmente acopladas. En cierto sentido, SRP es la cohesión definida desde el punto de vista de los usuarios.

Principio Abierto-Cerrado (OCP)

Las clases (y los módulos) deben aceptar la extensión de su funcionalidad, así como resistir las modificaciones de su funcionalidad actual. Vamos a jugar con el clásico ejemplo de un ventilador eléctrico. Tienes un interruptor y quieres controlar el ventilador. Entonces, podrías escribir algo como:

La herencia es probablemente la forma más fuerte de dependencia.

Este código define una clase Switch_ que crea y controla un objeto Fan. Ten en cuenta el subrayado después de "Switch_". PHP no te permite definir una clase con el nombre "Switch".

Tu jefe decide que quiere controlar la luz con el mismo interruptor. Esto es un problema, porque tienes que cambiar Switch_.

Cualquier modificación al código existente es un riesgo; otras partes del sistema pueden verse afectadas y requerir modificaciones adicionales. Siempre es preferible dejar la funcionalidad existente sola, cuando se agrega una nueva funcionalidad.

En la terminología de POO, puedes ver que Switch_ tiene una fuerte dependencia de Fan. Aquí es donde radica nuestro problema, y donde debemos hacer nuestros cambios.

Esta solución introduce la interfaz Switchable. Define los métodos que todos los objetos habilitados para conmutación necesitan implementar. Fan implementa Switchable, y Switch_ acepta una referencia a un objeto Switchable dentro de su constructor.

¿Cómo nos ayuda esto?

Primero, esta solución rompe la dependencia entre Switch_ y Fan. Switch_ no tiene idea de que inicia un ventilador, ni le importa. Segundo, la introducción de una clase Light no afectará a Switch_ ni a Switchable. ¿Quieres controlar un objeto Light con tu clase Switch_? Simplemente crea un objeto Light y pásalo a Switch_, como esto:

Principio de Sustitución de Liskov (LSP)

LSP establece que una clase secundaria nunca debe romper la funcionalidad de la clase principal. Esto es extremadamente importante porque los consumidores de una clase padre esperan que la clase se comporte de cierta manera. Pasar una clase secundaria a un consumidor debería funcionar y no afectar la funcionalidad original.

Esto es confuso a primera vista, así que veamos otro ejemplo clásico:

Este ejemplo define una clase Rectangle simple. Podemos establecer su altura y anchura, y su método area() proporciona el área del rectángulo. Usar la clase Rectangle podría verse así:

El método rectArea() acepta un objeto Rectangle como un argumento, establece su altura y ancho y devuelve el área de la forma.

En la escuela, nos enseñan que los cuadrados son rectángulos. Esto sugiere que si modelamos nuestro programa a nuestro objeto geométrico, una clase Square debería extender una clase Rectangle. ¿Cómo se vería una clase así?

Me resulta difícil descubrir qué escribir en la clase Square. Tenemos varias opciones. Podríamos anular el método area() y devolver el cuadrado de $width:

Ten en cuenta que cambié los campos del Rectangle a protected, dándole a Square a esos campos. Esto parece razonable desde un punto de vista geométrico. Un cuadrado tiene lados iguales; devolver el ancho del cuadrado es razonable.

Sin embargo, tenemos un problema desde un punto de vista de programación. Si Square es Rectangle, no deberíamos tener problemas para introducirlo en la clase Geometry . Pero, al hacerlo, puedes ver que el código Geometry no tiene mucho sentido; establece dos valores diferentes para altura y anchura. Es por esto que un cuadrado no es un rectángulo en la programación.

Principio de Segregación de Interfaz (ISP)

Las pruebas unitarias deben ejecutarse rápido, muy rápido.

Este principio se concentra en dividir interfaces grandes en interfaces pequeñas y especializadas. La idea básica es que los diferentes consumidores de la misma clase no deben conocer las diferentes interfaces, solo las interfaces que el consumidor necesita usar. Incluso si un consumidor no usa directamente todos los métodos públicos en un objeto, aún depende de todos los métodos. Entonces, ¿por qué no proporcionar interfaces que solo declaren los métodos que cada usuario necesita?

Esto está en estrecha conformidad con que las interfaces deben pertenecer a los clientes y no a la implementación. Si adaptas tus interfaces a las clases consumidoras, respetarán el ISP. La implementación en sí misma puede ser única, ya que una clase puede implementar varias interfaces.

Imaginemos que implementamos una aplicación bursátil. Tenemos un corredor que compra y vende acciones, y puede reportar sus ganancias y pérdidas diarias. Una implementación muy simple incluiría algo como una interfaz de Broker, una clase NYSEBroker que implementa Broker y un par de clases de interfaz de usuario: una para crear transacciones (TransactionsUI) y otra para informes (DailyReporter). El código para dicho sistema podría ser similar al siguiente:

Si bien este código puede funcionar, viola el ISP. Tanto DailyReporter como TransactionUI dependen de la interfaz de Broker. Sin embargo, cada uno solo usa una fracción de la interfaz. TransactionUI utiliza los métodos buy() y sell() , mientras que DailyReporter utiliza los métodos dailyEarnings() y dailyLoss().

Puedes argumentar que Broker no es cohesivo porque tiene métodos que no están relacionados y, por lo tanto, no se relacionan.

Esto puede ser cierto, pero la respuesta depende de las implementaciones de Broker; la venta y la compra pueden estar fuertemente relacionadas con las pérdidas y ganancias actuales. Por ejemplo, es posible que no se le permita comprar acciones si está perdiendo dinero.

También puedes argumentar que Broker también viola SRP. Debido a que tenemos dos clases que lo utilizan de diferentes maneras, puede haber dos usuarios diferentes. Bueno, yo digo que no. El único usuario es probablemente el corredor real. Él/ella quiere comprar, vender y ver sus fondos actuales. Pero, una vez más, la respuesta real depende de todo el sistema y la empresa.

ISP es seguramente violado. Ambas clases de interfaz de usuario dependen de todo el Broker. Este es un problema común, si crees que las interfaces pertenecen a tus implementaciones. Sin embargo, cambiar tu punto de vista puede sugerir el siguiente diseño:

Esto realmente tiene sentido y respeta el ISP. DailyReporter depende solo de BrokerStatistics; no le importa y no tiene que saber sobre ninguna operación de compra y venta. TransactionsUI, por otro lado, solo sabe sobre compras y ventas. El NYSEBroker es idéntico a nuestra clase anterior, excepto que ahora implementa las interfaces BrokerTransactions y BrokerStatistics.

Puedes pensar en ISP como un principio de cohesión de nivel superior.

Cuando ambas clases de UI dependían de la interfaz del Broker, eran similares a dos clases, cada una con cuatro campos, de los cuales dos se usaban en un método y las otras dos en otro. La clase no habría sido muy cohesiva.

Un ejemplo más complejo de este principio se puede encontrar en uno de los primeros artículos de Robert C. Martin sobre el tema: El Principio de Segregación de Interfaz.

Principio de inversión de dependencia (DIP)

Este principio establece que los módulos de alto nivel no deben depender de módulos de bajo nivel; ambos deben depender de las abstracciones. Las abstracciones no deben depender de los detalles; los detalles deben depender de las abstracciones. En pocas palabras, debes depender de las abstracciones tanto como sea posible y nunca de implementaciones concretas.

El truco con DIP es que deseas revertir la dependencia, pero siempre deseas mantener el flujo de control. Revisemos nuestro ejemplo de la OCP (las clases Switch y Light). En la implementación original, teníamos un interruptor que controlaba directamente una luz.

Como puedes ver, tanto la dependencia como el control fluyen de Switch a Light. Si bien esto es lo que queremos, no queremos depender directamente de Light. Así que introdujimos una interfaz.

Es sorprendente cómo la simple introducción de una interfaz hace que nuestro código respete tanto el DIP como el OCP. Como puedes ver, la clase depende de la implementación concreta de Light, y tanto Light como Switch dependen de la interfaz Switchable. Invertimos la dependencia, y el flujo de control se mantuvo sin cambios.


Diseño de alto nivel

Otro aspecto importante de tu código es el diseño de alto nivel y la arquitectura general. Una arquitectura enredada produce un código que es difícil de modificar. Mantener una arquitectura limpia es esencial, y el primer paso es comprender cómo separar las diferentes preocupaciones de tu código.

En esta imagen, intenté resumir las principales preocupaciones. En el centro del esquema está nuestra lógica de negocios. Debe estar bien aislado del resto del mundo y poder trabajar y comportarse como se espera sin la existencia de ninguna de las otras partes. Míralo como ortogonalidad en un nivel superior.

A partir de la derecha, tienes tu "main" (el punto de entrada a la aplicación) y las fábricas que crean objetos. Una solución ideal obtendría sus objetos de fábricas especializadas, pero eso es en su mayoría imposible o poco práctico. Aún así, debes usar las fábricas cuando tengas la oportunidad de hacerlo y mantenerlas fuera de tu lógica empresarial.

Luego, en la parte inferior (en naranja), tenemos persistencia (bases de datos, accesos de archivos, comunicaciones de red) con el propósito de persistir la información. Ningún objeto en nuestra lógica de negocios debería saber cómo funciona la persistencia.

A la izquierda está el mecanismo de entrega.

Un MVC, como Laravel o CakePHP, debería ser solo el mecanismo de entrega, nada más.

Esto te permite intercambiar un mecanismo por otro sin tocar la lógica de tu negocio. Esto puede sonar indignante para algunos de ustedes. Nos dicen que nuestra lógica de negocios debe ser colocada en nuestros modelos. Bueno, no estoy de acuerdo. Nuestros modelos deben ser "modelos de solicitud", es decir, objetos de datos simples utilizados para pasar información de MVC a la lógica de negocios. Opcionalmente, no veo ningún problema, incluida la validación de entrada en los modelos, pero nada más. La lógica de negocios no debe estar en los modelos.

Cuando observas la arquitectura de tu aplicación o la estructura de directorios, deberías ver una estructura que sugiere qué hace el programa en lugar de qué Framework o base de datos utilizaste.

Por último, asegúrate de que todas las dependencias apuntan hacia nuestra lógica empresarial. Las interfaces de usuario, las fábricas o factorías, las bases de datos son implementaciones muy concretas, y nunca debes depender de ellas. Invertir las dependencias para apuntar hacia nuestra lógica de negocios modulariza nuestro sistema, lo que nos permite cambiar las dependencias sin modificar la lógica de negocios.


Algunos pensamientos sobre patrones de diseño

Los patrones de diseño desempeñan un papel importante para facilitar la modificación del código, al ofrecer una solución de diseño común que todos los programadores pueden entender. Desde un punto de vista estructural, los patrones de diseño son obviamente ventajosos. Son soluciones bien probadas y pensadas.

Si deseas obtener más información sobre los patrones de diseño, ¡creé un curso Tuts + Premium en ellos!


La Fuerza de Testear

El desarrollo impulsado por pruebas fomenta la escritura de código que es fácil de probar. TDD te obliga a respetar la mayoría de los principios anteriores para que tu código sea fácil de probar. Inyectar dependencias y escribir clases ortogonales es esencial; de lo contrario, terminas con enormes métodos de prueba. Las pruebas unitarias deben ejecutarse rápido; en realidad, muy rápido, y todo lo que no se prueba debe ser burlado. Burlarse de muchas clases complejas para una prueba simple puede ser abrumador. Entonces, cuando te burlas de diez objetos para probar un solo método en una clase, puedes tener un problema con tu código... no con tu prueba.


Pensamientos finales

Al final del día, todo se reduce a cuánto te importa tu código fuente. Tener conocimientos técnicos no es suficiente; necesitas aplicar ese conocimiento una y otra vez, nunca estando 100% satisfecho con tu código. Debes querer que tu código sea fácil de mantener, limpiar y abrir para cambiar.

Gracias por leer y siéntete libre de contribuir con tus técnicas en los comentarios a continuación.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.