Creando Código Verdaderamente Modular sin Dependencias
Código interdependiente complejo, estrechamente unido y frágil. Todos lo hemos escrito. El tipo de código donde arreglar un error crea siete más. ¿Te has preguntado alguna vez cómo crear un código modular independiente?
En este artículo, el ingeniero freelance de software de Toptal Konrad Gadzinowski nos muestra los diferentes tipos de paradigmas arquitectónicos que puede cumplir y la forma de escribir un código modular y desacoplado donde los cambios en un módulo tienen un impacto mínimo en la aplicación general.
Código interdependiente complejo, estrechamente unido y frágil. Todos lo hemos escrito. El tipo de código donde arreglar un error crea siete más. ¿Te has preguntado alguna vez cómo crear un código modular independiente?
En este artículo, el ingeniero freelance de software de Toptal Konrad Gadzinowski nos muestra los diferentes tipos de paradigmas arquitectónicos que puede cumplir y la forma de escribir un código modular y desacoplado donde los cambios en un módulo tienen un impacto mínimo en la aplicación general.
Konrad specializes in creating modular, full-stack web applications that are easy to extend. His main expertise is Java and JavaScript.
Expertise
Previously At
El desarrollo de software es genial, pero… Creo que todos podemos estar de acuerdo en que puede ser una montaña rusa emocional. Al principio, todo es genial. Agrega nuevas características una tras otra en cuestión de días, si no de horas. ¡Estás en racha de suerte!
Avancemos rápido unos meses, y tu velocidad de desarrollo disminuye. ¿Es porque no estás trabajando tan duro como antes? Realmente no. Avancemos unos meses más y tu velocidad de desarrollo disminuirá aún más. Trabajar en este proyecto ya no es divertido y se ha convertido en un lastre.
Pero se pone peor. Empiezas a descubrir múltiples errores en tu aplicación. A menudo, resolver un error crea dos nuevos. En este punto, puedes comenzar a cantar:
99 pequeños errores en el código. 99 pequeños errores. Toma uno, ponle un parche,
…127 pequeños errores en el código.
¿Cómo te sientes al trabajar en este proyecto ahora? Si eres como yo, probablemente comiences a perder tu motivación. Desarrollar esta aplicación es complicado, 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 descartar su código fuente y volver a escribir todo.
Razones por las Cuales 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 su 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.
Entonces ahí lo tienes; la verdadera causa de nuestros problemas es aumentar la complejidad proveniente de todas las dependencias que tiene nuestro código.
La Gran Pelota de Barro y Cómo Reducirla
Lo curioso es que este problema se conoce desde hace años. Es un antipatrón común llamado la “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 compañías diferentes.
Entonces, ¿qué es este antipatrón exactamente? Simplemente hablando, obtienes 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. Solo trace una línea entre cada par de clases que dependen el uno del otro. Ahora puedes ver la fuente de tus problemas.
Una Solución con Código Modular
Entonces me hice una pregunta: ¿sería posible reducir la complejidad y aún divertirme como al comienzo del proyecto? A decir verdad, no puedes eliminar todos la complejidad. Si desea agregar nuevas características, siempre tendrá que aumentar la complejidad del código. Sin embargo, la complejidad puede moverse y separarse.
Cómo otras Industrias están Resolviendo este Problema
Piensa 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 combina. Pueden hacer esos componentes completamente por separado y ensamblar todo al final, haciendo solo algunos retoques. ¿Cómo es esto posible? Saben cómo cada elemento se ajustará según los estándares de la industria, como el tamaño de los pernos, y las decisiones iniciales como el tamaño de los orificios de montaje y la distancia entre ellos.
Cada elemento en el conjunto anterior puede ser proporcionado por una empresa independiente 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?
¡Seguro 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 debe apegarse a algunas reglas simples y mantenerse disciplinado.
La inversión del Control es tu Amigo
Cuando escuché por primera vez sobre la inversión del 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, pueden trabajar juntos sin saber nada el uno del otro. Es brillante.
Veamos en un ejemplo simple cómo podemos desacoplar nuestro sistema para crear código modular. Los diagramas siguientes se han implementado como simples aplicaciones Java. Puede 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 sola clase Util
. Esos elementos dependen el uno del otro de múltiples maneras. A continuación, puede ver una implementación usando el enfoque de “gran bola de barro”. Las clases simplemente se llaman entre sí. Están estrechamente unidos, y no se puede simplemente sacar un elemento sin tocar a los demás. Las aplicaciones creadas con este estilo le permiten crecer inicialmente rápidamente. Creo que este estilo es apropiado para proyectos de prueba de concepto, ya que puedes jugar con facilidad. Sin embargo, no es apropiado para soluciones listas para producción porque incluso el mantenimiento puede ser peligroso y cualquier cambio puede crear errores impredecibles. El siguiente diagrama muestra esta gran bola de arquitectura de barro.
¿Por Qué la Inyección de Dependencia lo Hizo Todo Mal?
En una búsqueda de un mejor enfoque, podemos usar una técnica llamada inyección de dependencia. Este método supone que todos los componentes se deben usar a través de interfaces. He leído afirmaciones de que desacopla elementos, pero ¿realmente lo hace? No. Echale 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 directamente a las clases, las llamamos a través de sus interfaces. Mejora ligeramente los elementos de separación entre sí. Si, por ejemplo, desea reutilizar Servicio A
en un proyecto diferente, puede hacerlo sacando Servicio A
, junto con Interfaz A
, así como Interfaz B
y Interface Útil
. Como puede ver, el Servicio A
todavía depende de otros elementos. Como resultado, todavía tenemos problemas para cambiar el código en un lugar y desordenar el comportamiento en otro. Todavía crea el problema de que si modifica Servicio B
e Interfaz B
, necesitará 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. ¡Hurra por la independencia!
La Solución Para el 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. Tú creas un componente y su oyente. Un oyente es una interfaz simple. Siempre que necesites llamar a un método desde fuera del elemento actual, simplemente agrega un método al oyente y llámelo en su lugar. El elemento solo tiene permitido usar archivos, llamar a métodos dentro de su paquete y usar clases proporcionadas por el marco principal u otras bibliotecas usadas. A continuación, puedes ver un diagrama de la aplicación modificada para usar la arquitectura de elementos.
Ten en cuenta que, en esta arquitectura, solo la clase Main
tiene múltiples dependencias. Conecta todos los elementos y encapsula la lógica de negocios de la aplicación.
Los servicios, por otro lado, son elementos completamente independientes. Ahora, puedes 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 cambie su comportamiento. Mientras esos servicios hagan lo que se supone que deben hacer, pueden dejarse intactos hasta el final de los tiempos. Pueden ser creados por un ingeniero profesional de software, o un codificador por primera vez comprometido con el peor código de espagueti que alguien haya cocinado con declaraciones de goto
mezcladas. No importa, porque su lógica está encapsulada. Por horrible que sea, nunca se extenderá a otras clases. Eso también le da el poder de dividir el trabajo en un proyecto entre múltiples desarrolladores, donde cada desarrollador puede trabajar en su propio componente de forma independiente sin la necesidad de interrumpir otro o incluso saber sobre la existencia de otros desarrolladores.
Finalmente, puedes comenzar a escribir código independiente una vez más, al igual que al comienzo de tu último proyecto.
Element Pattern
Definamos el patrón del elemento estructural 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 deseas usar un elemento, entonces necesitas implementar el oyente y realizar llamadas a la clase principal. Aquí hay un diagrama de la configuración más simple:
Obviamente, necesitarás agregar más complejidad en el elemento eventualmente, pero puedes hacerlo fácilmente. Solo asegúrate de que ninguna de tus clases de lógica dependa de otros archivos en el proyecto. Solo pueden usar el marco principal, las bibliotecas importadas y otros archivos en este elemento. Cuando se trata de archivos de activos como imágenes, vistas, sonidos, etc., también deben estar encapsulados dentro de los elementos para que en el futuro sean fáciles de reutilizar. ¡Simplemente puedes copiar la carpeta completa a otro proyecto y allí está!
A continuación, puedes ver un gráfico de ejemplo que muestra un elemento más avanzado. Ten en cuenta que consiste en una vista que está usando y no depende de ningún otro archivo de aplicación. Si deseas conocer un método simple para verificar dependencias, solo mire la sección de importación. ¿Hay algún archivo desde fuera del elemento actual? De ser así, debes eliminar esas dependencias moviéndolas al elemento o agregando una llamada apropiada al oyente.
Echemos un vistazo a un ejemplo simple de “Hello World” 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
. Ten en cuenta 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 fácilmente en diferentes aplicaciones.
Ahora echale un vistazo a la clase principal de App
. Implementa el oyente y ensambla el elemento junto con la implementación concreta. Ahora podemos comenzar a usarlo.
También puedes ejecutar este ejemplo en JavaScript aquí
Arquitectura de Elemento
Echemos un vistazo al uso del patrón de elementos en aplicaciones a gran escala. Una cosa es mostrarlo en un proyecto pequeño; 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 del cliente y del servidor. Es algo razonable de hacer, 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 aplicaciones y elementos. Los elementos constan de carpetas con componentes independientes, mientras que la carpeta de la aplicación conecta todos los elementos y almacena toda la lógica comercial.
De esta forma, 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 con frecuencia se reduce a simples llamadas a elementos.
Ejemplos Prácticos
Si creemos que la práctica siempre prevalece sobre la teoría, echemos un vistazo a un ejemplo de la vida real creado en Node.js y TypeScript.
Es una aplicación web muy simple que puede usarse como punto de partida para soluciones más avanzadas. Sigue la arquitectura del elemento y utiliza un patrón de elementos extensivamente estructural.
A partir de los aspectos más destacados, puede ver que la página principal se ha distinguido como un elemento. Esta página incluye su propia vista. Entonces, cuando, por ejemplo, deseas reutilizarlo, puede simplemente copiar la carpeta completa y soltarla en un proyecto diferente. Simplemente conecta todo y estarás listo.
Es un ejemplo básico que demuestra que puede comenzar a introducir elementos en su propia aplicación hoy. Puedes comenzar a distinguir componentes independientes y separar su lógica. No importa cuán desordenado sea el código en el que esté trabajando actualmente.
¡Desarrollar más rápido, reutilizar más a menudo!
Espero que, con este nuevo conjunto de herramientas, puedas desarrollar más fácilmente código que sea más fácil de mantener. Antes de saltar al uso del patrón de elementos en la práctica, repasemos rápidamente todos los puntos principales:
-
Se producen muchos problemas en el software debido a las dependencias entre varios componentes.
-
Al hacer un cambio en un lugar, puedes introducir un comportamiento impredecible en otro lugar.
Los tres enfoques arquitectónicos comunes son:
-
La gran bola de barro. Es ideal para un desarrollo rápido, pero no tan bueno para fines de producción estable.
-
Inyección de dependencia. Es una solución a medias 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 estables de producción.
El patrón de elemento básico 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 elemento de pila completa, primero se separa el front-end del código de back-end. Luego, crea una carpeta en cada uno para una aplicación y elementos. La carpeta de elementos consta de todos los elementos independientes, mientras que la carpeta de aplicaciones conecta todo junto.
Ahora puedes ir y comenzar a crear y compartir tus propios elementos. A largo plazo, te ayudará a crear productos fáciles de mantener. ¡Buena suerte y déjame saber lo que has creado!
Además, si te encuentras prematuramente optimizando tu código, lee _ Cómo evitar la maldición de la optimización prematura_ del mi compañero de Toptal, Kevin Bloch.
Konrad Gadzinowski
Łódź, Poland
Member since August 10, 2017
About the author
Konrad specializes in creating modular, full-stack web applications that are easy to extend. His main expertise is Java and JavaScript.
Expertise
PREVIOUSLY AT