Crear Código Verdaderamente Modular sin Dependencias
Desarrollar software es genial, pero think creo que todos estamos de acuerdo en que puede ser una montaña rusa emocional. Al principio, todo es genial. Agrega nuevas funciones una tras otra en cuestión de días, si no de horas. ¡Estás en racha!
Avance rápido unos meses, y su velocidad de desarrollo disminuye. Es porque no estás trabajando tan duro como antes? En realidad no. Avancemos unos meses más y su velocidad de desarrollo disminuirá aún más. Trabajar en este proyecto ya no es divertido y se ha convertido en una carga.
Empeora. Comienza a descubrir varios errores en su aplicación. A menudo, resolver un error crea dos nuevos. En este punto, puedes empezar a cantar:
99 pequeños bichos en el código.99 bichitos.Toma uno, pásalo,
1 127 pequeños errores en el código.
¿Qué te parece trabajar en este proyecto ahora? Si eres como yo, probablemente empieces a perder la motivación. Es difícil desarrollar esta aplicación, ya que cada cambio en el código existente puede tener consecuencias impredecibles.
Esta experiencia es común en el mundo del software y puede explicar por qué tantos programadores quieren desechar su código fuente y reescribirlo todo.
- Razones Por las que el desarrollo de software se Ralentiza con el tiempo
- Gran bola de barro y Cómo Reducirla
- Una Solución con Código Modular
- Cómo Otras Industrias Están Resolviendo Este Problema
- La inversión de Control Es Tu Amiga
- Problema
- Por qué la Inyección de Dependencias se Equivocó
- La Solución para Código Modular
- Patrón de elementos
- Arquitectura de elementos
- Ejemplo práctico
- ¡Desarrolle Más Rápido, Reutilice Más A Menudo!
Razones Por las que el desarrollo de software se Ralentiza con el tiempo
Entonces, ¿cuál es la razón de este problema?
La causa principal es la creciente complejidad. Desde mi experiencia, el mayor contribuyente a la complejidad general es el hecho de que, en la gran mayoría de los proyectos de software, todo está conectado. Debido a las dependencias que tiene cada clase, si cambia algún código en la clase que envía correos electrónicos, sus usuarios de repente no pueden registrarse. ¿Por qué es eso? Porque tu código de registro depende del código que envía los correos electrónicos. Ahora no puedes cambiar nada sin introducir errores. Simplemente no es posible rastrear todas las dependencias.
Así que ahí lo tienes; la verdadera causa de nuestros problemas es aumentar la complejidad proveniente de todas las dependencias que tiene nuestro código.
Gran bola de barro y Cómo Reducirla
Lo curioso es que este problema se conoce desde hace años. Es un anti-patrón común llamado “gran bola de barro”.”He visto ese tipo de arquitectura en casi todos los proyectos en los que trabajé a lo largo de los años en múltiples empresas diferentes.
¿Qué es exactamente este anti-patrón? En pocas palabras, se obtiene una gran bola de barro cuando cada elemento tiene una dependencia con otros elementos. A continuación, puede ver un gráfico de las dependencias del conocido proyecto de código abierto Apache Hadoop. Para visualizar la gran bola de barro (o más bien, la gran bola de hilo), dibuja un círculo y coloca las clases del proyecto de manera uniforme en él. Simplemente dibuja una línea entre cada par de clases que dependen unas de otras. Ahora puedes ver la fuente de tus problemas.
Una Solución con Código Modular
Así que me hice una pregunta: ¿es posible reducir la complejidad y divertirse como al principio del proyecto? A decir verdad, no se puede eliminar toda la complejidad. Si desea agregar nuevas características, siempre tendrá que aumentar la complejidad del código. Sin embargo, la complejidad se puede mover y separar.
Cómo Otras Industrias Están Resolviendo Este Problema
Piense en la industria mecánica. Cuando un pequeño taller mecánico crea máquinas, compra un conjunto de elementos estándar, crea algunos personalizados y los junta. Pueden hacer esos componentes completamente por separado y ensamblar todo al final, haciendo solo unos pocos ajustes. ¿Cómo es posible? Saben cómo encajará cada elemento mediante estándares industriales establecidos, como tamaños de pernos, y decisiones iniciales, como el tamaño de los orificios de montaje y la distancia entre ellos.
Cada elemento en el ensamblaje anterior puede ser proporcionado por una empresa separada que no tiene ningún conocimiento sobre el producto final o sus otras piezas. Siempre que cada elemento modular se fabrique de acuerdo con las especificaciones, podrá crear el dispositivo final según lo planeado.
¿Podemos replicar eso en la industria del software?
¡Claro que podemos! Mediante el uso de interfaces y la inversión del principio de control; la mejor parte es el hecho de que este enfoque se puede utilizar en cualquier lenguaje orientado a objetos: Java, C#, Swift, TypeScript, JavaScript, PHP-la lista sigue y sigue. No necesita ningún marco elegante para aplicar este método. Solo tiene que atenerse a algunas reglas simples y mantenerse disciplinado.
La inversión de Control Es Tu Amiga
Cuando escuché por primera vez sobre la inversión de control, inmediatamente me di cuenta de que había encontrado una solución. Es un concepto de tomar dependencias existentes e invertirlas mediante el uso de interfaces. Las interfaces son simples declaraciones de métodos. No proporcionan ninguna implementación concreta. Como resultado, se pueden usar como un acuerdo entre dos elementos sobre cómo conectarlos. Se pueden usar como conectores modulares, si se quiere. Mientras un elemento proporcione la interfaz y otro elemento proporcione la implementación para ella, pueden trabajar juntos sin saber nada el uno del otro. Es brillante.
Veamos en un ejemplo sencillo cómo podemos desacoplar nuestro sistema para crear código modular. Los diagramas a continuación se han implementado como aplicaciones Java simples. Puedes encontrarlos en este repositorio de GitHub.
Problema
Supongamos que tenemos una aplicación muy simple que consiste solo en una clase Main
, tres servicios y una única clase Util
. Esos elementos dependen unos de otros de múltiples maneras. A continuación, puede ver una implementación que utiliza el enfoque de” bola grande de barro”. Las clases simplemente se llaman entre sí. Están estrechamente acoplados, y no puedes simplemente sacar un elemento sin tocar otros. Las aplicaciones creadas con este estilo le permiten crecer rápidamente inicialmente. Creo que este estilo es apropiado para proyectos de prueba de concepto, ya que puedes jugar con las cosas fácilmente. Sin embargo, no es apropiado para soluciones listas para la producción porque incluso el mantenimiento puede ser peligroso y cualquier cambio individual puede crear errores impredecibles. El siguiente diagrama muestra esta gran bola de arquitectura de barro.
Por qué la Inyección de Dependencias se Equivocó
En una búsqueda de un mejor enfoque, podemos usar una técnica llamada inyección de dependencias. Este método asume que todos los componentes deben usarse a través de interfaces. He leído afirmaciones de que desacopla elementos, pero, ¿realmente lo hace? No. Eche un vistazo al diagrama a continuación.
La única diferencia entre la situación actual y una gran bola de barro es el hecho de que ahora, en lugar de llamar a las clases directamente, las llamamos a través de sus interfaces. Mejora ligeramente los elementos de separación entre sí. Si, por ejemplo, desea reutilizar Service A
en un proyecto diferente, puede hacerlo eliminando Service A
, junto con Interface A
, así como Interface B
y Interface Util
. Como puede ver, Service A
todavía depende de otros elementos. Como resultado, todavía tenemos problemas para cambiar el código en un lugar y estropear el comportamiento en otro. Todavía crea el problema de que si modifica Service B
y Interface B
, tendrá que cambiar todos los elementos que dependen de él. Este enfoque no resuelve nada; en mi opinión, solo agrega una capa de interfaz sobre los elementos. Nunca debe inyectar dependencias, sino que debe deshacerse de ellas de una vez por todas. ¡Viva la independencia!
La Solución para Código Modular
El enfoque que creo que resuelve todos los principales dolores de cabeza de las dependencias lo hace al no usar dependencias en absoluto. Se crea un componente y su oyente. Un oyente es una interfaz sencilla. Siempre que necesite llamar a un método desde fuera del elemento actual, simplemente agregue un método al receptor y llámelo en su lugar. Al elemento solo se le permite usar archivos, llamar a métodos dentro de su paquete y usar clases proporcionadas por el framework principal u otras bibliotecas usadas. A continuación, puede ver un diagrama de la aplicación modificada para usar la arquitectura de elementos.
Tenga en cuenta que, en esta arquitectura, solo la clase Main
tiene múltiples dependencias. Conecta todos los elementos y encapsula la lógica de negocio de la aplicación.
Los servicios, por otro lado, son elementos completamente independientes. Ahora, puede sacar cada servicio de esta aplicación y reutilizarlos en otro lugar. No dependen de nada más. Pero espera, se pone mejor: No necesitas modificar esos servicios nunca más, siempre y cuando no cambies su comportamiento. Mientras esos servicios hagan lo que se supone que deben hacer, pueden quedar intactos hasta el final de los tiempos. Pueden ser creados por un ingeniero de software profesional, o un codificador por primera vez comprometido con el peor código espagueti que alguien haya cocinado con goto
frases mezcladas. No importa, porque su lógica está encapsulada. Por horrible que pueda ser, nunca se extenderá a otras clases. Eso también le da el poder de dividir el trabajo en un proyecto entre varios desarrolladores, donde cada desarrollador puede trabajar en su propio componente de forma independiente sin la necesidad de interrumpir a otro o incluso saber sobre la existencia de otros desarrolladores.
Finalmente, puede comenzar a escribir código independiente una vez más, al igual que al principio de su último proyecto.
Patrón de elementos
Definamos el patrón de elementos estructurales para que podamos crearlo de manera repetible.
La versión más simple del elemento consta de dos cosas: Una clase de elemento principal y un oyente. Si desea utilizar un elemento, debe implementar el receptor y realizar llamadas a la clase principal. Aquí hay un diagrama de la configuración más simple:
Obviamente, tendrá que agregar más complejidad al elemento eventualmente, pero puede hacerlo fácilmente. Solo asegúrese de que ninguna de sus clases de lógica dependa de otros archivos del proyecto. Solo pueden usar el framework principal, las bibliotecas importadas y otros archivos de este elemento. Cuando se trata de archivos activos como imágenes, vistas, sonidos, etc., también deben estar encapsulados dentro de elementos para que en el futuro sean fáciles de reutilizar. Simplemente puede copiar toda la carpeta a otro proyecto y ¡ahí está!
A continuación, puede ver un gráfico de ejemplo que muestra un elemento más avanzado. Observe que consiste en una vista que está utilizando y no depende de ningún otro archivo de aplicación. Si desea conocer un método simple para verificar dependencias, simplemente mire la sección importar. ¿Hay archivos fuera del elemento actual? Si es así, debe eliminar esas dependencias moviéndolas al elemento o agregando una llamada apropiada al receptor.
Echemos también un vistazo a un ejemplo simple de” Hola Mundo ” creado en Java.
public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String args) { App app = new App(); app.start(); }}
Inicialmente, definimos ElementListener
para especificar el método que imprime la salida. El elemento en sí se define a continuación. Al llamar a sayHello
en el elemento, simplemente imprime un mensaje usando ElementListener
. Observe que el elemento es completamente independiente de la implementación del método printOutput
. Se puede imprimir en la consola, una impresora física o una interfaz de usuario elegante. El elemento no depende de esa implementación. Debido a esta abstracción, este elemento se puede reutilizar en diferentes aplicaciones fácilmente.
Ahora eche un vistazo a la clase principal App
. Implementa al oyente y ensambla el elemento junto con la implementación concreta. Ahora podemos empezar a usarlo.
También puede ejecutar este ejemplo en JavaScript aquí
Arquitectura de elementos
Echemos un vistazo al uso del patrón de elementos en aplicaciones a gran escala. Una cosa es mostrarlo en un pequeño proyecto, y otra es aplicarlo al mundo real.
La estructura de una aplicación web de pila completa que me gusta usar se ve de la siguiente manera:
src├── client│ ├── app│ └── elements│ └── server ├── app └── elements
En una carpeta de código fuente, inicialmente dividimos los archivos cliente y servidor. Es algo razonable, ya que se ejecutan en dos entornos diferentes: el navegador y el servidor de fondo.
Luego dividimos el código en cada capa en carpetas llamadas app y elements. Elements consiste en carpetas con componentes independientes, mientras que la carpeta de la aplicación conecta todos los elementos y almacena toda la lógica de negocio.
De esta manera, los elementos se pueden reutilizar entre diferentes proyectos, mientras que toda la complejidad específica de la aplicación se encapsula en una sola carpeta y, a menudo, se reduce a simples llamadas a elementos.
Ejemplo práctico
Creyendo que la práctica siempre triunfa sobre la teoría, echemos un vistazo a un ejemplo de la vida real creado en Node.js y TypeScript.
Ejemplo de la vida real
Es una aplicación web muy sencilla que se puede utilizar como punto de partida para soluciones más avanzadas. Sigue la arquitectura de elementos, así como utiliza un patrón de elementos estructurales extensivamente.
En highlights, puede ver que la página principal se ha distinguido como un elemento. Esta página incluye su propia vista. Así que cuando, por ejemplo, quieras reutilizarlo, simplemente puedes copiar toda la carpeta y colocarla en un proyecto diferente. Conéctalo todo y listo.
Es un ejemplo básico que demuestra que puede comenzar a introducir elementos en su propia aplicación hoy mismo. Puede comenzar a distinguir componentes independientes y separar su lógica. No importa lo desordenado que sea el código en el que esté trabajando actualmente.
¡Desarrolle Más Rápido, Reutilice Más A Menudo!
Espero que, con este nuevo conjunto de herramientas, pueda desarrollar más fácilmente código que sea más fácil de mantener. Antes de empezar a usar el patrón de elementos en la práctica, recapitulemos rápidamente todos los puntos principales:
-
Muchos problemas en el software ocurren debido a las dependencias entre múltiples componentes.
-
Al hacer un cambio en un lugar, puedes introducir un comportamiento impredecible en otro lugar.
Tres enfoques arquitectónicos comunes son:
-
La gran bola de barro. Es ideal para un desarrollo rápido, pero no tan grande para fines de producción estables.
-
Inyección de dependencia. Es una solución a medio hornear que debes evitar.
-
Arquitectura de elementos. Esta solución le permite crear componentes independientes y reutilizarlos en otros proyectos. Es mantenible y brillante para lanzamientos de producción estables.
El patrón de elementos básicos consiste en una clase principal que tiene todos los métodos principales, así como un oyente que es una interfaz simple que permite la comunicación con el mundo externo.
Para lograr una arquitectura de elementos de pila completa, primero separe su front-end del código back-end. A continuación, crea una carpeta en cada una de ellas para una aplicación y elementos. La carpeta elements consta de todos los elementos independientes, mientras que la carpeta app conecta todo.
Ahora puedes empezar a crear y compartir tus propios elementos. A largo plazo, le ayudará a crear productos fácilmente mantenibles. ¡Buena suerte y hazme saber lo que has creado!
Además, si se encuentra optimizando prematuramente su código, lea Cómo Evitar la Maldición de la Optimización Prematura de su compañero Toptaler Kevin Bloch.