Read in English · Lire en français · Auf Deutsch lesen
Sección 3.1 de 3.8 · Intermedio · 22 min de lectura
Modularidad: dividir un sistema en partes
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Todo sistema grande está dividido en partes. En esta lección aprenderás qué es un módulo, por qué dividimos los sistemas en módulos y la regla más importante para decidir dónde van los límites.
Piensa en un hospital grande. No es una sala enorme en la que todo el mundo hace de todo. Está dividido en departamentos, como radiología y farmacia, cada uno con su propio personal. Los demás se relacionan con cada departamento a través de un mostrador, con formularios de solicitud claros. Así que, si la farmacia cambia su sistema de existencias, las plantas no lo notan, porque siguen rellenando el mismo formulario. El software se divide en módulos de la misma manera. Si se hace bien, las personas pueden entender un módulo sin leer el resto, los equipos pueden trabajar en paralelo y un cambio se queda en un solo lugar. Pero solo si las paredes están en los lugares adecuados.
Un módulo es una parte de un sistema con una responsabilidad clara y una forma definida en que las demás partes pueden usarla. Eso no dice nada del tamaño: un módulo puede ser una función, una clase, un paquete, un componente o un servicio entero. Su interfaz es lo que los demás deben saber para usarlo. Su implementación es todo lo demás. Una buena modularidad mantiene la interfaz pequeña y estable, para que la implementación pueda cambiar libremente.
En 1972, David Parnas demostró cuánto importa la elección de los límites, usando un pequeño programa que construye un índice de palabras clave en contexto, para que un lector pueda encontrar un título por cualquiera de sus palabras. El primer diseño sigue el diagrama de flujo, con un módulo para cada paso del procesamiento: entrada, rotación circular, alfabetización y salida. Es un diseño perfectamente razonable. Pero todos los módulos conocen los formatos de datos compartidos en memoria. Así que un cambio en la forma de guardar las líneas toca los cuatro. El segundo diseño parece más extraño. Sus módulos no coinciden con los pasos. En cambio, cada uno oculta una sola decisión de diseño, como el formato de entrada o el momento en que se ordena, tras funciones de acceso. Ahora un cambio en la forma de guardar las líneas se queda dentro de un solo módulo, el almacén de líneas. Los demás solo llaman a sus funciones.
Parnas concluyó que casi siempre es un error partir de un diagrama de flujo. En su lugar, enumera las decisiones de diseño que son difíciles o que probablemente cambien, y dale a cada módulo una de ellas para que la oculte. A esto lo llamó ocultación de información. Un diagrama de flujo describe el tiempo: primero esto, después aquello. Pero las decisiones de diseño atraviesan el tiempo. Cómo se representa un pedido importa cuando se recibe, se tarifica, se paga y se guarda. Si un módulo es propietario de esa decisión, puede cambiar en un solo lugar.
A continuación, una distinción importante. Un módulo lógico es una unidad de responsabilidad en el diseño, como Catálogo o Pagos. Un módulo físico es una unidad que se compila y se despliega, como un paquete o una imagen de contenedor. Los mismos módulos lógicos se pueden desplegar como una sola unidad, llamándose entre sí dentro del mismo proceso: rápido y con un solo lanzamiento. Esto es un monolito modular. O cada uno puede ser su propio servicio, al que se llama a través de la red. Cada uno se puede lanzar por separado, pero lo pagas en latencia, fallos parciales y versionado. Dividir en servicios un sistema mal dividido no oculta nada. Solo te da servicios enredados, lo que a veces se llama monolito distribuido. Acierta primero con los límites lógicos, y solo entonces decide cuáles merecen convertirse en físicos.
Pongamos esto a prueba con el proceso de pago de una tienda en línea. Un diseño tiene seis pasos de diagrama de flujo que comparten un solo registro de pedido. El otro tiene seis módulos, cada uno de los cuales oculta una decisión. Contamos los módulos que tocarían cinco cambios previstos. Un segundo proveedor de pagos toca dos pasos del diagrama de flujo, pero solo el módulo de pasarela de pago. Un cambio en las reglas del IVA toca tres pasos, pero solo el módulo de Impuestos. Dividir la dirección de entrega en campos estructurados toca los seis pasos, porque todos leen el registro compartido. En el otro diseño, solo cambia Pedido. Algunos cambios cuestan lo mismo en ambos casos. Las confirmaciones por SMS tocan un módulo en los dos diseños, y una funcionalidad realmente nueva, un mensaje de regalo, toca tres en los dos. En total, quince frente a siete. Enumerar los cambios probables y contar los módulos que toca cada uno es una forma rápida de comparar diseños en una revisión.
¿Qué tamaño debe tener un módulo? Los módulos demasiado grandes mezclan motivos de cambio sin relación entre sí, y muchas personas editan los mismos archivos. Los módulos demasiado pequeños multiplican las interfaces, así que una sola funcionalidad pequeña necesita cambios en muchos lugares. A través de una red, cada interfaz también cuesta latencia, versionado y una nueva forma de fallar, así que los servicios deben ser mucho más gruesos que las clases. Y un límite que solo existe en un diagrama no sobrevivirá a un plazo de entrega. Los profesionales lo hacen cumplir con reglas de visibilidad, sistemas de módulos y comprobaciones automáticas de dependencias que hacen fallar la compilación.
La sección completa trata en detalle el ejemplo de Parnas y cómo hace cumplir los límites cada lenguaje. A continuación, en la sección tres punto dos, veremos el acoplamiento y la cohesión. Nos vemos allí.
Lo que necesitas saber
- Un módulo es una unidad de descomposición con una responsabilidad y una interfaz: una función, una clase, un paquete, un componente o un servicio, según la escala a la que estés trabajando.
- La modularidad rinde frutos gracias a la comprensión, el trabajo en paralelo de los equipos, el aislamiento del cambio, la reutilización y las pruebas, pero solo si los límites se trazan en los lugares adecuados.
- David Parnas (1972) demostró con el ejemplo KWIC que descomponer por pasos de procesamiento reparte cada decisión de diseño entre muchos módulos, y propuso en su lugar que cada módulo oculte una decisión que sea difícil o que probablemente vaya a cambiar.
- Los módulos lógicos (unidades de responsabilidad en el diseño) son distintos de los módulos físicos (unidades de compilación y de despliegue); el mismo diseño lógico se puede desplegar como una sola unidad o como muchos servicios.
- La granularidad es un compromiso: los módulos demasiado grandes mezclan cambios que no tienen relación entre sí, mientras que los módulos demasiado pequeños multiplican las interfaces, la coordinación y, a través de una red, la latencia y los modos de fallo.
- Los límites que no se hacen cumplir se erosionan, así que los profesionales usan la visibilidad del lenguaje, sistemas de módulos como el Java Platform Module System, los módulos de las herramientas de compilación y comprobaciones automáticas de dependencias.
Qué es un módulo
Un módulo es una parte de un sistema que tiene una responsabilidad clara y una forma definida en que las demás partes pueden usarla. Esa definición es deliberadamente independiente del tamaño. En la Sección 0.3 un módulo era un archivo de Python; en este capítulo la palabra abarca cualquier unidad de descomposición, el acto de dividir un sistema en partes: una función, una clase, un paquete, una biblioteca, un componente dentro de una aplicación o un servicio desplegado por separado. Las mismas preguntas valen a todas las escalas: ¿de qué es responsable esta parte, qué ofrece a las demás, qué se guarda para sí y de qué depende?
Todo módulo tiene dos caras. Su interfaz es lo que otros módulos deben saber para usarlo: nombres y parámetros de funciones, una API HTTP, los eventos que publica y también las promesas informales, como lo rápido que responde y qué errores puede devolver. Su implementación es todo lo demás: el código, las estructuras de datos, las tablas de la base de datos, las bibliotecas de terceros que use. Una buena modularidad mantiene la interfaz pequeña y estable y deja que la implementación cambie libremente.
David Parnas, cuyo artículo de 1972 es la base de esta sección, definió un módulo como una asignación de responsabilidad y no como un fragmento de texto de programa. Esa es la forma más útil de pensar en él como arquitecto. Un módulo es la promesa de que una parte del sistema, y normalmente un equipo, es responsable de un conjunto de decisiones. Que el código acabe en un archivo o en cuarenta, en un proceso o en diez, es otra cuestión, que se trata más adelante en el apartado sobre módulos lógicos y físicos.
Por qué modularizar
Un sistema de unos pocos cientos de líneas se puede entender en su conjunto. Un sistema de unos pocos millones de líneas no, y tampoco la organización que lo construye puede trabajar como un único grupo indiviso. La modularidad es la propiedad de estar dividido en módulos que se pueden entender, cambiar y analizar en gran medida por separado. Es una de las cinco subcaracterísticas de la mantenibilidad en ISO/IEC 25010 (Sección 2.6) y aporta al menos cinco beneficios distintos.
Comprensión. Un desarrollador puede estudiar un módulo y sus interfaces sin leer el resto del sistema. Trabajo en paralelo. Los equipos pueden trabajar en módulos distintos al mismo tiempo sin apenas necesidad de coordinarse, porque han acordado las interfaces de antemano. Aislamiento del cambio. Un cambio en una decisión de diseño, como qué normas fiscales se aplican, se hace en un módulo y no se propaga hacia fuera. Reutilización. Un módulo bien delimitado, como un componente de validación de direcciones, puede usarse en varios productos. Pruebas. Un módulo con una interfaz clara puede probarse por sí solo, sustituyendo sus dependencias por dobles de prueba.
Parnas enumeró tres de ellos en 1972: el de gestión (el tiempo de desarrollo se acorta porque grupos separados trabajan en módulos separados con poca comunicación), la flexibilidad (cambios drásticos en un módulo sin cambiar los demás) y la comprensibilidad (estudiar el sistema módulo a módulo). Medio siglo después, siguen siendo las razones. Pero fíjate en que ninguno de ellos se deriva automáticamente de cortar un sistema en trozos. Un sistema puede estar dividido en cincuenta módulos y no ofrecer ninguno de estos beneficios, si cada cambio toca diez de ellos. Los beneficios dependen por completo de dónde se tracen los límites.
Parnas y el ejemplo KWIC
Parnas lo demostró con un pequeño programa llamado KWIC (key word in context, palabra clave en contexto), que genera un índice de títulos. Lee líneas de texto, genera todas las rotaciones circulares de cada línea (pasando la primera palabra al final, una y otra vez), ordena alfabéticamente todas las rotaciones y las imprime. La línea Design of Software produce Design of Software, of Software Design y Software Design of, de modo que un lector puede encontrar el título por cualquiera de sus palabras.
La primera descomposición que describe es la que produciría la mayoría de los programadores dibujando un diagrama de flujo: un módulo de entrada (Input) que lee las líneas y las guarda en memoria en un formato compactado, un módulo de rotación circular (Circular Shift) que construye un índice de dónde empieza cada rotación, un módulo de alfabetización (Alphabetising) que ordena ese índice, un módulo de salida (Output) que imprime el resultado y un módulo de control maestro que los llama en orden. Es un diseño perfectamente razonable. Cada módulo es pequeño y las interfaces entre ellos son los formatos de datos en memoria.
La segunda descomposición parece más extraña. Tiene un almacén de líneas (Line Storage) que ofrece funciones como dame el carácter de la línea r, palabra w, posición c y oculta cómo se guardan realmente las líneas; un módulo de entrada que lee líneas y se las entrega al almacén de líneas; un rotador circular (Circular Shifter) que presenta las rotaciones como si estuvieran guardadas, mediante funciones de acceso similares; un alfabetizador (Alphabetiser) que ofrece dame la i-ésima rotación en orden sin decir cuándo ni cómo ordena; un módulo de salida; y un control maestro. Los módulos ya no corresponden a los pasos del procesamiento.
Después, Parnas enumeró decisiones que eran discutibles y que probablemente cambiarían: el formato de entrada; mantener todas las líneas en memoria, lo que no es práctico con entradas muy grandes; compactar varios caracteres en cada palabra de máquina; guardar un índice de las rotaciones en lugar de las propias rotaciones; y ordenarlo todo de una vez en lugar de buscar bajo demanda. En la primera descomposición, la mayoría de estos cambios afectan a varios módulos, porque todos los módulos conocen los formatos de datos compartidos. En la segunda, cada cambio queda confinado al único módulo que oculta esa decisión. El segundo diseño también era más fácil de desarrollar en paralelo, porque los equipos solo tenían que acordar nombres y parámetros de funciones, no estructuras de tablas detalladas, y más fácil de entender módulo a módulo.
Las dos descomposiciones de KWIC de Parnas
El criterio: descomponer por decisiones que probablemente cambien
La conclusión de Parnas fue que es casi siempre incorrecto
empezar a descomponer un sistema a partir de un diagrama de flujo. En su lugar, propuso partir de una lista de decisiones de diseño difíciles, o de decisiones de diseño que probablemente vayan a cambiar, y diseñar cada módulo para que oculte una de esas decisiones a los demás. Llamó a este principio ocultación de información; la Sección 3.3 lo trata en profundidad.
La idea parece sencilla, pero invierte el instinto que tenemos la mayoría. Un diagrama de flujo describe el tiempo: primero ocurre esto, después aquello. Las decisiones de diseño atraviesan el tiempo. La decisión sobre cómo se representa un pedido se usa cuando el pedido se recibe, se tarifica, se paga, se guarda y se confirma. Si los módulos siguen los pasos, esa única decisión se reparte entre todos ellos, y cambiarla significa cambiarlos todos. Si un módulo es propietario de la decisión, todos los pasos le preguntan a ese módulo, y la decisión puede cambiar en un solo lugar.
En la práctica importan dos matices. Primero, el criterio se refiere al cambio probable, lo que significa que un arquitecto tiene que hacer una suposición informada sobre el futuro: qué ha cambiado en sistemas similares, qué planea el negocio, qué proveedores y qué normativas son inestables. Las cuatro preguntas de modificabilidad de la Sección 2.6 (qué cambia, con qué probabilidad, quién lo cambia y a qué costo) son exactamente la información que esto necesita. Segundo, Parnas distinguió entre una buena descomposición y una jerarquía. Un sistema puede estar limpiamente organizado en capas y aun así mal descompuesto, y al revés; la Sección 3.4 trata por separado la dirección de las dependencias.
Módulos lógicos y módulos físicos
Un módulo lógico es una unidad de responsabilidad en el diseño: Catálogo, Pedidos, Pagos. Un módulo físico es una unidad que se compila, se versiona o se despliega: un archivo JAR de Java, un wheel de Python, un paquete npm, una imagen de contenedor, una función serverless. La Sección 1.1 las llamó estructuras de módulos y estructuras de asignación. Las dos están relacionadas, pero no son lo mismo, y confundirlas provoca algunos de los errores más caros de la arquitectura moderna.
El mismo diseño lógico se puede corresponder con módulos físicos de maneras muy diferentes. Todos los módulos lógicos pueden vivir en una sola unidad desplegable, llamándose entre sí dentro del mismo proceso: un monolito modular. O cada uno puede desplegarse como su propio servicio, llamando a los demás a través de la red: microservicios. Los límites lógicos, y las decisiones que oculta cada módulo, pueden ser idénticos en ambos casos. Lo que cambia es el costo de cruzar un límite (una llamada a una función tarda nanosegundos; una llamada de red, milisegundos, y puede fallar), la capacidad de lanzar y escalar las partes de forma independiente y el trabajo de operación.
Un diseño lógico, dos diseños físicos
Un error común y costoso es tratar la separación física como si creara modularidad lógica. Dividir en servicios un sistema mal descompuesto no oculta ninguna decisión; convierte las llamadas dentro del proceso entre módulos enredados en llamadas de red entre servicios enredados, lo que a veces se llama monolito distribuido. También se da el error contrario: un equipo mantiene una sola unidad desplegable, pero deja que sus módulos internos crezcan unos dentro de otros, de modo que después ya nunca podrá separarlos. El consejo práctico de muchos profesionales es acertar primero con los límites lógicos, hacerlos cumplir y solo entonces decidir cuáles de ellos merecen convertirse en límites físicos.
La granularidad y sus costos
¿Qué tamaño debe tener un módulo? La granularidad es el tamaño de los módulos en relación con el sistema, y no hay una respuesta correcta universal, porque los costos tiran en direcciones opuestas. Los módulos más grandes tienen menos interfaces que diseñar y mantener, pero cada uno contiene más motivos de cambio sin relación entre sí, más personas editándolo a la vez y más cosas que entender antes de hacer un cambio. Los módulos más pequeños son, cada uno, más fáciles de entender, pero el sistema tiene más interfaces, más dependencias que gestionar y más lugares en los que hay que ensamblar una misma funcionalidad.
El costo de una interfaz también depende del tipo de límite. Entre dos clases de un mismo programa, una interfaz adicional cuesta un poco de tiempo de lectura. Entre dos servicios cuesta un salto de red, serialización, una política de versionado, monitorización, un modo de fallo y, posiblemente, el acuerdo de un segundo equipo para cada cambio. Por eso la granularidad adecuada para los servicios suele ser mucho más gruesa que la granularidad adecuada para las clases.
Síntomas de una granularidad equivocada
| Demasiado gruesa | Demasiado fina |
|---|---|
| Funcionalidades sin relación entre sí cambian el mismo módulo; muchas personas editan los mismos archivos y entran en conflicto en el control de versiones. | Una sola funcionalidad pequeña necesita cambios coordinados en muchos módulos o servicios. |
| Tiempos largos de compilación y de pruebas para cualquier cambio; nadie entiende el módulo entero. | Muchas interfaces triviales de paso que no añaden ninguna abstracción; cadenas de llamadas que atraviesan varios módulos. |
| Los equipos hacen cola para lanzar porque todo sale a la vez. | A través de una red: latencia alta por llamadas excesivamente frecuentes, fallos parciales y transacciones distribuidas. |
El nombre del módulo es vago, como core, common o manager. | Cada módulo es superficial: su interfaz es casi tan compleja como su implementación (consulta la Sección 3.3). |
Hacer cumplir los límites de los módulos en el código
Un límite que solo existe en un diagrama no sobrevivirá al choque con un plazo de entrega. Tarde o temprano alguien importará una clase interna porque es más rápido, y en cuanto existe un atajo le siguen otros; la Sección 1.7 llamó a esto erosión. Los lenguajes y las herramientas de compilación ofrecen varias formas de hacer que el límite previsto sea el camino de menor resistencia, o de hacer que cruzarlo haga fallar la compilación.
Python se basa en convenciones: los nombres que empiezan con un guion bajo se consideran internos (PEP 8), y el __init__.py de un paquete puede volver a exportar solo sus funciones públicas. Nada impide un import de shop.payments._stripe_client, así que los equipos a los que les importa usan un comprobador como import-linter para hacer fallar la compilación cuando aparece un import prohibido. Java ofrece la visibilidad de paquete (package-private, la predeterminada cuando no se indica ningún modificador) y, desde Java 9 en 2017, el Java Platform Module System: un archivo module-info.java declara qué módulos requires este y cuáles de sus paquetes exports; los paquetes que no se exportan no se pueden usar desde fuera, ni en tiempo de compilación ni en tiempo de ejecución. Go trata cualquier paquete situado bajo un directorio llamado internal como importable solo desde el código que cuelga del directorio padre de ese directorio. C# tiene internal, visible solo dentro de un ensamblado; Rust tiene pub(crate).
# shop/payments/__init__.py (the public face of the package)
from shop.payments.api import charge, refund, PaymentResult
__all__ = ["charge", "refund", "PaymentResult"]
# shop/payments/_provider_client.py internal: leading underscore
# shop/payments/_retry.py internal
# shop/payments/api.py the only module callers should use
# Elsewhere in the shop:
from shop.payments import charge # fine
from shop.payments._retry import backoff # works, but breaks the boundary// module-info.java for the payments module
module shop.payments {
requires shop.orders.api; // what this module depends on
exports shop.payments.api; // the only package others can use
// shop.payments.provider and shop.payments.retry are not exported,
// so code in other modules cannot compile against them
}Formas de hacer cumplir un límite
| Mecanismo | Ejemplos | Fuerza |
|---|---|---|
| Convención de nombres | Guion bajo inicial en Python; internal en el nombre de un espacio de nombres | Débil: depende de la disciplina y de la revisión de código |
| Visibilidad del lenguaje | Package-private de Java, internal de C#, pub(crate) de Rust, directorios internal de Go | Fuerte dentro de una compilación; la comprueba el compilador |
| Sistema de módulos del lenguaje | Java Platform Module System (requires, exports); el campo exports de un package.json de Node.js | Fuerte: un acceso no declarado no compila o no se carga |
| Módulos de la herramienta de compilación | Compilaciones multimódulo de Maven o Gradle; reglas de visibilidad de Bazel; paquetes separados en un monorrepositorio | Fuerte: un módulo solo puede usar lo que declara como dependencia |
| Comprobaciones automáticas de arquitectura | import-linter, ArchUnit, dependency-cruiser (Sección 3.4) | Fuerte si se ejecuta en el pipeline de compilación como función de aptitud |
| Límite de proceso y de red | Servicios separados con sus propios almacenes de datos, accesibles solo a través de una API | El más fuerte, y el más caro de cruzar y de cambiar |
Ejemplo práctico
Ejemplo práctico: descomponer una tienda en línea por diagrama de flujo y por cambios probables
El proceso de pago de una tienda en línea se diseña primero como un diagrama de flujo: Recibir pedido, Validar carrito, Calcular precio (promociones e IVA), Cobrar el pago, Guardar pedido, Enviar confirmación. Los seis pasos leen y escriben un único registro de pedido compartido, un diccionario cuyas claves conocen todos los pasos. El diseño alternativo tiene seis módulos, cada uno de los cuales oculta una decisión: Pedido (cómo se representa y se guarda un pedido), Precios (reglas de promociones), Impuestos (reglas del IVA), Pasarela de pago (qué proveedor y cómo se le llama), Notificaciones (canales y plantillas) y un Flujo de compra que solo ordena las llamadas, como el control maestro de Parnas. Se cuentan cinco cambios que el negocio espera para el año próximo frente a ambos diseños.
| Cambio 1: añadir un segundo proveedor de pagos | Diagrama de flujo: Cobrar el pago, más Guardar pedido, que almacena el formato de la referencia de transacción del proveedor = 2 módulos. Por cambio: Pasarela de pago = 1. |
|---|---|
| Cambio 2: cambian las reglas del IVA de una categoría de productos | Diagrama de flujo: Calcular precio, Guardar pedido (columnas del IVA) y Enviar confirmación (desglose del IVA) = 3. Por cambio: Impuestos = 1. |
| Cambio 3: dividir la dirección de entrega en campos estructurados | Diagrama de flujo: todos los pasos leen el registro de pedido compartido = 6. Por cambio: Pedido = 1 (los demás llaman a sus métodos). |
| Cambio 4: añadir confirmaciones por SMS | Diagrama de flujo: Enviar confirmación = 1. Por cambio: Notificaciones = 1. |
| Cambio 5: una funcionalidad nueva, un mensaje de regalo en los pedidos | Diagrama de flujo: Recibir pedido, Guardar pedido, Enviar confirmación = 3. Por cambio: Flujo de compra, Pedido, Notificaciones = 3. |
| Total de módulos afectados | Diagrama de flujo: 2 + 3 + 6 + 1 + 3 = 15. Por cambio: 1 + 1 + 1 + 1 + 3 = 7. |
Los cambios en decisiones existentes (proveedor, reglas fiscales, representación) se quedan dentro de un módulo en el diseño basado en el cambio; eso es exactamente lo que predijo Parnas.
Algunos cambios cuestan lo mismo en ambos diseños (SMS), y una funcionalidad realmente nueva (el mensaje de regalo) cruza módulos en los dos. La ocultación de información protege las decisiones que anticipaste, no cualquier funcionalidad posible.
El recuento es una forma rápida y repetible de comparar descomposiciones candidatas en una revisión de diseño: primero enumera los cambios probables y después cuenta los módulos que toca cada uno.
Errores comunes
Descomponer solo por capa técnica. Las carpetas llamadas controllers, services y repositories son módulos en cierto modo, pero un cambio en cómo funciona el IVA tocará las tres. Las capas son útiles (Sección 3.4), pero una descomposición que solo tiene capas es un diagrama de flujo disfrazado. La mayoría de los sistemas maduros combinan una división por capacidad de negocio con capas dentro de cada parte.
Todo compartido. Un módulo llamado common o utils que importan todos los demás módulos se convierte en el lugar donde se acumulan decisiones sin relación entre sí, y cualquier cambio en él se propaga por todas partes. Límites que no se hacen cumplir. Los diagramas muestran módulos limpios mientras el código tiene cientos de imports cruzados. Módulos definidos por el organigrama de una reorganización pasada en lugar de por las decisiones que el sistema tiene que ocultar; la Sección 3.7 explica por qué las organizaciones y los límites de los módulos se influyen mutuamente. Distribución prematura. Convertir cada módulo lógico en un servicio antes de que los límites hayan demostrado ser estables encarece las correcciones posteriores de esos límites.
Lista de comprobación: revisar una descomposición propuesta
- Nombra la decisión que oculta cada móduloSi no puedes decir en una frase qué mantiene en secreto un módulo, probablemente sea un paso de procesamiento o un cajón de sastre.
- Enumera los cambios probablesAnota entre cinco y diez cambios que probablemente traigan el negocio o la tecnología, y cuenta los módulos que tocaría cada uno.
- Comprueba el tamaño de la interfazLa interfaz debe ser mucho más pequeña que la implementación. Las interfaces grandes con poco comportamiento detrás sugieren que el corte es equivocado.
- Separa lo lógico de lo físicoAcuerda primero los módulos lógicos; decide por separado cuáles merecen su propia unidad desplegable y por qué.
- Decide cómo se hará cumplir el límiteElige un mecanismo de visibilidad o una comprobación automática, y añádelo a la compilación antes de que aparezca el primer atajo.
- Haz coincidir los límites con la propiedadCada módulo debe tener un equipo propietario; los módulos que comparten varios equipos para la mayor parte de su trabajo son una señal de alarma.
Cómo aplican la modularidad los profesionales
Los arquitectos con experiencia tratan la estructura de módulos como el artefacto de diseño más importante para la mantenibilidad, y la revisan una y otra vez. Al principio de la vida de un producto, cuando el dominio se entiende mal, prefieren menos módulos, más grandes y con límites internos fuertes, porque fusionar y dividir módulos dentro de una misma base de código es barato. A medida que el dominio se estabiliza y los cambios probables se aclaran, afinan los límites y solo entonces estudian cuáles deberían desplegarse por separado. Las técnicas del diseño guiado por el dominio, como los contextos delimitados, ayudan a encontrar límites que siguen el lenguaje del negocio.
También miden. Contar los módulos que han tocado los cambios recientes, algo que el historial del control de versiones facilita, muestra si los límites funcionan; la Sección 3.8 presenta el acoplamiento de cambio y los hotspots. La siguiente sección, la Sección 3.2, ofrece el vocabulario para describir cómo son los límites buenos y los malos: el acoplamiento entre módulos y la cohesión dentro de ellos.
Comprueba lo que has entendido
¿Por qué Parnas llamó a un módulo una "asignación de responsabilidad" y no un subprograma?
Porque lo que importa es de qué decisiones de diseño es responsable un módulo y cuáles oculta, no cómo se empaqueta su código; una responsabilidad puede abarcar varias rutinas, y las rutinas no tienen por qué coincidir con los pasos del procesamiento.
En el ejemplo KWIC, ¿por qué cambiar la forma de guardar las líneas afectaba a varios módulos en la primera descomposición?
Todos los módulos compartían directamente los formatos de datos en memoria, así que todos los módulos conocían la decisión de almacenamiento en lugar de estar oculta en uno solo.
¿Cuál es la diferencia entre un módulo lógico y un módulo físico?
Un módulo lógico es una unidad de responsabilidad en el diseño; un módulo físico es una unidad que se compila o se despliega, como un JAR, un paquete, un contenedor o un servicio. Un mismo diseño lógico se puede empaquetar físicamente de muchas maneras.
Nombra dos señales de que los servicios tienen una granularidad demasiado fina.
Las funcionalidades pequeñas necesitan cambios coordinados en varios servicios, y las solicitudes implican cadenas de llamadas de red excesivamente frecuentes, con latencia alta y fallos parciales.
Fuentes y lecturas recomendadas
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “¿Cómo decido dónde trazar los límites de los módulos en un monolito existente antes de plantearnos dividirlo en servicios?”