Read in English · Lire en français · Auf Deutsch lesen
Sección 3.2 de 3.8 · Intermedio · 29 min de lectura · Última revisión: noviembre de 2026
Acoplamiento y cohesión
Videolección
5 min · narrado en español, con subtítulosSolo escuchar (audio)
Leer la transcripción
El acoplamiento y la cohesión son las dos medidas más antiguas de una buena estructura. En esta lección aprenderás sus niveles clásicos, cómo contar las dependencias, los tipos adicionales de acoplamiento entre servicios y cómo debilitarlo.
Piensa en dos vecinos que comparten un jardín. Si solo se prestan alguna herramienta de vez en cuando, cualquiera de los dos puede rediseñar su propio jardín sin problemas. Están débilmente acoplados. Si comparten un cobertizo, una manguera y una llave debajo de una maceta, cada cambio exige una conversación. Están fuertemente acoplados. Ahora piensa en un cajón de la cocina. Si solo contiene cubiertos, sabes dónde buscar. Es cohesivo. Si contiene cubiertos, pilas, cordel y recibos viejos, no lo es. Un buen software tiene partes cohesivas que están débilmente acopladas entre sí.
El acoplamiento es el grado en que un módulo depende de otro, de modo que un cambio en uno puede obligar a cambiar el otro. La cohesión es el grado en que los elementos de un módulo forman un todo, porque sirven a un único propósito. Pero el acoplamiento no es un defecto. Un sistema cuyas partes nunca dependieran unas de otras no haría nada útil. El objetivo es elegirlo de forma deliberada: pocas dependencias, a través de interfaces estrechas y estables, y tan débil como permita la tarea.
El diseño estructurado, de la década de 1970, ordenó las formas en que pueden conectarse los módulos, de la más fuerte, a la izquierda, a la más débil, a la derecha. El acoplamiento de contenido consiste en meterse en las partes internas de otro módulo, como un servicio que escribe en las tablas de la base de datos de otro servicio. El acoplamiento común consiste en compartir datos globales, o un mismo esquema de base de datos. El acoplamiento externo consiste en compartir un formato impuesto, como la estructura de archivo de un socio. El acoplamiento de control consiste en pasar un indicador que le dice al otro módulo qué hacer. El acoplamiento de marca pasa un registro entero cuando solo se necesita una parte. El acoplamiento de datos, el más débil, pasa solo datos simples que se necesitan todos, a través de una interfaz estrecha y documentada. La cohesión pregunta por qué están juntas las cosas de un módulo. En el extremo débil, la coincidental: un módulo utils que contiene el formateo de fechas y un tipo de IVA. En el extremo fuerte, la funcional: un módulo Tax que calcula el IVA, y solo eso.
Los niveles describen el tipo de dependencia. Los arquitectos también las cuentan. El acoplamiento aferente es el número de módulos que dependen de un componente: sus flechas entrantes. El acoplamiento eferente es el número de módulos de los que depende: sus flechas salientes. Los términos vienen de la anatomía, donde los nervios aferentes llevan las señales hacia dentro y los eferentes, hacia fuera. Un componente con un acoplamiento aferente alto, como una biblioteca de dinero que usan cuarenta módulos, es responsable. Cualquier cambio puede romper muchos otros, así que debe cambiar con cuidado y rara vez. Uno con un acoplamiento eferente alto, como un flujo de compra que llama a otros cinco módulos, está expuesto. El problema llega cuando un componente es ambas cosas.
Entre servicios aparecen tres tipos más, y ninguno se ve en la firma de una función. El acoplamiento temporal significa que una parte solo funciona si otra está disponible en ese mismo momento. Si el proceso de pago llama directamente a pagos y pagos está caído, el proceso de pago también lo está. El acoplamiento de despliegue significa que dos partes deben lanzarse juntas. Si cada lanzamiento de Pedidos necesita un lanzamiento de Facturación, en la práctica son una sola unidad, diga lo que diga el diagrama. El acoplamiento semántico consiste en estar de acuerdo en un significado que ninguna interfaz expresa, como que el estado cuatro significa enviado, o que los precios están en peniques. Estos acuerdos se rompen en silencio.
Veamos una refactorización práctica. Antes, el módulo Pedidos de una tienda en línea llama directamente a Facturación en cada pedido y le pasa un indicador de modo. Eso es acoplamiento de control, y también acoplamiento temporal: Pedidos espera a Facturación. Facturación también lee directamente la tabla de pedidos, y descifra sus códigos de estado y su columna del IVA. Eso es acoplamiento común, y también acoplamiento semántico. Después, Pedidos publica un evento Order Placed a través de un intermediario de mensajes, con exactamente los datos que necesita Facturación, incluidos los totales que Pedidos ya ha calculado. Cada servicio es propietario de sus propios datos. Lo que queda es acoplamiento de datos, a través de un contrato pequeño y publicado. Y Pedidos ya no espera a Facturación.
Midamos lo que ha cambiado. Antes, una tabla compartida, un indicador de modo y códigos de estado ocultos. Después, un solo evento versionado con cinco campos. Antes, un pedido fallaba si Facturación estaba caída. Ahora el evento espera en el broker hasta que Facturación se recupera. Antes, cambiar la tabla de pedidos exigía un lanzamiento conjunto. Ahora Pedidos puede cambiarla libremente. Pero hay costos nuevos: un broker que operar, facturas unos segundos más tarde y un evento que ahora es un contrato público. El acoplamiento no se eliminó, se eligió. Ahora es explícito, estrecho y versionado.
La sección completa tiene tablas de todos los niveles, un ejemplo práctico en el que se cuentan dependencias y las tácticas para debilitar el acoplamiento, cada una con su precio. A continuación, en la sección tres punto tres, veremos la ocultación de información. Nos vemos allí.
Lo que necesitas saber
- El acoplamiento es el grado en que un módulo depende de otro; la cohesión es el grado en que los elementos de un módulo forman un todo. Busca un acoplamiento débil y una cohesión alta, que normalmente van juntos.
- Las escalas del diseño estructurado ordenan el acoplamiento de contenido, común, externo, de control y de marca hasta el acoplamiento de datos (del más fuerte al más débil), y la cohesión coincidental, lógica, temporal, procedimental, comunicacional y secuencial hasta la funcional (de la más débil a la más fuerte).
- El acoplamiento aferente (Ca) cuenta las dependencias entrantes, y el acoplamiento eferente (Ce), las salientes; un módulo con un Ca alto es arriesgado de cambiar, y uno con un Ce alto está expuesto a los cambios de los demás.
- Los sistemas distribuidos añaden el acoplamiento temporal (ambas partes deben estar disponibles a la vez), el acoplamiento de despliegue (deben lanzarse juntas) y el acoplamiento semántico (deben compartir un significado oculto), que una API limpia por sí sola no elimina.
- Entre las tácticas que reducen el acoplamiento están las interfaces estrechas, los intermediarios como los adaptadores y los brokers, los eventos en lugar de los comandos y una propiedad estricta de los datos, cada una con su propio costo.
- El acoplamiento no se puede eliminar, solo elegir: parte del acoplamiento es la razón de ser del sistema, así que el objetivo es hacerlo explícito, débil y local donde el cambio es probable.
Dos medidas de la estructura
El acoplamiento es el grado en que un módulo depende de otro, de modo que un cambio en uno puede obligar a cambiar el otro. La cohesión es el grado en que los elementos de un módulo forman un todo porque sirven a un único propósito. Ambas ideas las introdujo Larry Constantine a finales de la década de 1960 como parte del movimiento del diseño estructurado, y se publicaron en el artículo Structured Design de Wayne Stevens, Glenford Myers y Constantine en el IBM Systems Journal de 1974, y después con más extensión en el libro de Edward Yourdon y Constantine Structured Design (1979). La Sección 0.3 las presentó de manera informal; esta sección las precisa.
Las dos están relacionadas. Si las cosas que cambian juntas viven en el mismo módulo (cohesión alta), menos cambios cruzan los límites entre módulos, y los módulos necesitan saber menos unos de otros (acoplamiento bajo). Yourdon y Constantine llegaron a decir que, en la práctica, la cohesión era la más importante de las dos. Las tácticas de modificabilidad de la Sección 2.6 (aumentar la cohesión, reducir el acoplamiento) se basan exactamente en esta pareja.
Una aclaración desde el principio: el acoplamiento no es un defecto. Un sistema cuyos módulos nunca dependieran unos de otros no haría nada útil. Martin Fowler, que escribió sobre el tema en 2001, advirtió que intentar eliminar por completo el acoplamiento solo empuja todo a un módulo enorme en el que el acoplamiento está oculto, no desaparecido. El objetivo es elegir el acoplamiento de forma deliberada: pocas dependencias, a través de interfaces estrechas y estables, que apunten en direcciones sensatas y tan débiles como permita la tarea.
Las escalas clásicas
Los niveles de acoplamiento
El diseño estructurado ordenó las formas en que pueden conectarse dos módulos, de la más perjudicial a la más inofensiva. La forma de seis niveles que aparece a continuación suele atribuirse a los libros de Glenford Myers sobre diseño compuesto de mediados de la década de 1970, y más tarde la popularizó Meilir Page-Jones; los nombres y los límites varían ligeramente entre fuentes, pero esta es la forma que recogen la mayoría de los libros de texto. Se definieron para procedimientos dentro de un programa, pero cada uno tiene un equivalente directo entre componentes y servicios, que es donde los encuentran hoy los arquitectos.
Niveles de acoplamiento, del más fuerte al más débil
| Nivel | Qué significa | En el código | Entre servicios |
|---|---|---|---|
| Contenido | Un módulo se mete en las partes internas de otro: cambia sus datos privados o salta dentro de su código. | Asignar cart._items desde fuera; aplicar un monkey-patch a la función de otro módulo. | Leer o escribir directamente en las tablas de la base de datos de otro servicio. |
| Común | Varios módulos leen y escriben los mismos datos globales. | Un diccionario global config que cualquier función puede cambiar. | Varios servicios que comparten un esquema de base de datos o un espacio de claves de caché compartido. |
| Externo | Los módulos comparten un formato, un protocolo o un dispositivo impuestos desde fuera. | Dos módulos analizan el mismo formato de archivo de un proveedor. | Varios servicios que dependen de la estructura de archivo de un socio o de una versión concreta de un protocolo de mensajes. |
| Control | Un módulo pasa un indicador que le dice al otro qué hacer, de modo que quien llama conoce la lógica interna de quien es llamado. | process(order, mode="refund") | Una API con un campo action que selecciona comportamientos completamente distintos. |
| Marca | Un módulo recibe una estructura de datos entera, pero solo usa una parte. | Pasar el objeto Customer completo a una función que solo necesita la dirección de correo electrónico. | Enviar el registro completo del cliente en cada evento cuando los consumidores necesitan dos campos. |
| Datos | Los módulos se comunican solo mediante parámetros de datos simples, todos ellos necesarios. | send_receipt(email, order_id, total) | Una API o un evento estrecho y documentado, con solo los campos que necesitan los consumidores. |
Dos advertencias. Primero, la escala es una guía del riesgo, no una ley. El acoplamiento de marca suele ser un intercambio sensato: pasar un objeto entero es más sencillo que desempaquetar seis campos, y el único costo es que el receptor puede romperse ahora cuando cambian campos que ni siquiera usa. Segundo, la escala no dice nada sobre cuántas dependencias hay ni sobre la frecuencia con que cambia la otra parte. Una sola llamada con acoplamiento de control a un módulo que nunca cambia puede ser inofensiva; el acoplamiento de datos con un módulo que cambia cada semana puede hacer daño. Más adelante en este capítulo, la connascence ofrece un vocabulario más fino, y Balancing Coupling in Software Design (2024), de Vlad Khononov, sostiene que la fuerza del acoplamiento debe juzgarse siempre junto con la distancia entre los módulos y su volatilidad.
# Control coupling: the caller chooses the callee's internal branch
def handle_payment(order, mode):
if mode == "charge":
... # call the provider, store the transaction
elif mode == "refund":
... # look up the transaction, reverse it
elif mode == "dry_run":
... # validate only
handle_payment(order, "refund")
# Data coupling: separate operations, each taking only what it needs
def charge(order_id: str, amount_pence: int) -> str: ...
def refund(transaction_id: str, amount_pence: int) -> None: ...
refund("tx_81f2", 1999)Los niveles de cohesión
La cohesión pregunta por qué están juntos los elementos de un módulo. Yourdon y Constantine describieron siete tipos de asociación, también de peor a mejor. Los niveles inferiores agrupan cosas por razones que no tienen que ver con el problema (se escribieron por casualidad al mismo tiempo, o se ejecutan en el mismo momento); los niveles superiores agrupan cosas porque trabajan sobre los mismos datos o hacia un mismo propósito único. Los resúmenes posteriores de los libros de texto coinciden en el orden general: la cohesión coincidental y la lógica son pobres, la comunicacional y la secuencial son buenas y la funcional es la mejor, aunque los saltos entre niveles no están espaciados de manera uniforme.
Niveles de cohesión, del más débil al más fuerte
| Nivel | Los elementos están agrupados porque... | Ejemplo |
|---|---|---|
| Coincidental | no hay ninguna relación significativa entre ellos. | Un módulo utils que contiene el formateo de fechas, un decorador de reintentos y un tipo de IVA. |
| Lógica | hacen el mismo tipo de cosa, seleccionada mediante un indicador. | Un módulo exports con una función que escribe CSV, PDF o XML según un parámetro. |
| Temporal | se ejecutan al mismo tiempo. | Una rutina startup() que abre la base de datos, precarga las cachés y envía un ping de monitorización. |
| Procedim. | se suceden en una secuencia de control, aunque sobre datos distintos. | Una función que valida un formulario, después registra la dirección IP del usuario y después actualiza un menú. |
| Comunicacional | operan sobre los mismos datos. | Un módulo que calcula varios informes a partir del mismo historial de pedidos. |
| Secuencial | la salida de uno es la entrada del siguiente. | Analizar un extracto bancario, después clasificar sus líneas y después totalizar cada categoría. |
| Funcional | juntos realizan una tarea bien definida y nada más. | Un módulo Tax que calcula el IVA de un carrito, y solo eso. |
A escala de arquitectura, la cohesión es la cuestión de si un componente o un servicio tiene una sola razón para existir. Un servicio llamado CustomerService que gestiona el inicio de sesión, los puntos de fidelidad y las preferencias de marketing tiene, como mucho, cohesión lógica: tres equipos lo cambiarán por tres motivos sin relación entre sí. Una buena heurística es describir el propósito de un módulo en una sola frase sin usar la palabra y; la Sección 3.5 desarrolla esta idea como el principio de responsabilidad única, definido en función de las personas que solicitan los cambios.
Acoplamiento aferente y eferente
Los niveles anteriores describen el tipo de una dependencia. Los arquitectos también necesitan contarlas. El acoplamiento aferente (Ca) es el número de módulos externos a un componente que dependen de él: sus flechas entrantes. El acoplamiento eferente (Ce) es el número de módulos externos de los que depende: sus flechas salientes. Los términos vienen de la anatomía, donde los nervios aferentes llevan las señales hacia dentro y los eferentes, hacia fuera. Robert C. Martin los definió para paquetes en 1994 contando clases; las herramientas actuales cuentan clases, módulos o paquetes, así que comprueba siempre qué cuenta una herramienta antes de comparar cifras.
Las dos cifras cuentan historias distintas. Un componente con un Ca alto, como una biblioteca compartida money que usan cuarenta módulos, es responsable: cualquier cambio en él puede romper muchos otros, así que debe cambiar con cuidado y rara vez. Un componente con un Ce alto, como un flujo de compra que llama a precios, existencias, pagos, entregas y notificaciones, está expuesto: puede necesitar cambios cada vez que cambie cualquiera de ellos. Ninguna de las dos situaciones es mala en sí misma; el problema surge cuando de un componente dependen muchos otros y, a la vez, él mismo depende de muchas cosas volátiles. La Sección 3.4 convierte Ca y Ce en la métrica de inestabilidad de Martin y en las reglas que se basan en ella.
Ejemplo práctico
Ejemplo práctico: contar Ca y Ce en una app de reparto de comida
Siete módulos (Caja, Carta, Precios, Pagos, Notificaciones, Seguimiento de repartidores y Administración) tienen estas dependencias ("A depende de B" se escribe "A a B"): Caja a Carta, Caja a Precios, Caja a Pagos, Caja a Notificaciones; Seguimiento de repartidores a Notificaciones; Precios a Carta; Administración a Carta, Administración a Precios.
| Carta | Ca = 3 (Caja, Precios, Administración); Ce = 0 |
|---|---|
| Precios | Ca = 2 (Caja, Administración); Ce = 1 (Carta) |
| Notificaciones | Ca = 2 (Caja, Seguimiento de repartidores); Ce = 0 |
| Pago | Ca = 0; Ce = 4 (Carta, Precios, Pagos, Notificaciones) |
| Lectura | Carta es el módulo del que más se depende, así que su interfaz necesita el máximo cuidado y los cambios más lentos. Caja es el que más depende de otros, así que debería ser el más fácil de cambiar y el que absorba los cambios de los demás. |
Contar lleva unos minutos con una herramienta de dependencias y convierte una impresión vaga ("Carta es central") en una cifra que puedes seguir a lo largo del tiempo.
El acoplamiento en los sistemas distribuidos
Cuando los módulos se convierten en servicios que se comunican a través de una red, los niveles clásicos siguen siendo válidos, pero cobran importancia otros tres tipos de acoplamiento. Ninguno de ellos es visible en la firma de una función, por eso los equipos que dividen un sistema en servicios suelen sorprenderse de lo acoplados que siguen estando los servicios.
El acoplamiento temporal significa que una parte solo funciona si otra está disponible y responde en ese mismo momento. Una llamada HTTP síncrona de Caja a Pagos acopla su disponibilidad: si Pagos cae, también cae el proceso de pago, y la disponibilidad combinada es el producto de las dos (Sección 2.5). Los mensajes asíncronos a través de una cola eliminan este acoplamiento, a cambio de resultados eventuales en lugar de inmediatos. (En el diseño a nivel de código, el mismo término significa a veces que las llamadas deben hacerse en un orden fijo, como open() antes de read(); normalmente el contexto deja claro el significado).
El acoplamiento de despliegue significa que dos partes deben lanzarse juntas, o en un orden fijo, porque un cambio en una rompe la otra. Si cada lanzamiento de Pedidos necesita un lanzamiento simultáneo de Facturación, en la práctica los dos son una sola unidad desplegable, diga lo que diga el diagrama. Los cambios compatibles con versiones anteriores, las API versionadas y el patrón expandir y contraer son los remedios habituales.
El acoplamiento semántico significa que dos partes deben estar de acuerdo en un significado que ninguna interfaz expresa: que un pedido con status = 4 está enviado, que los precios están en peniques e incluyen el IVA, que un "cliente activo" ha hecho un pedido en los últimos 90 días. Estos acuerdos se rompen en silencio cuando una de las partes cambia su interpretación. El remedio es hacer explícito el significado en el contrato (estados con nombre, unidades en los nombres de los campos, esquemas publicados) o mantener la regla en un único lugar al que los demás consultan.
Tácticas para reducir el acoplamiento
Interfaces estrechas. Expón el menor número de operaciones y campos que necesiten los clientes. Cada campo adicional en una respuesta de una API es algo de lo que un cliente puede llegar a depender (Sección 3.3, sobre la ley de Hyrum). Intermediarios. Pon algo entre las dos partes para que ninguna conozca los detalles de la otra: un adaptador o una capa anticorrupción que traduzca el modelo de un proveedor, un API gateway o un intermediario de mensajes (message broker) que desacople al emisor del receptor en el tiempo y en la identidad. Eventos en lugar de comandos. Un comando (Facturación, crea una factura) obliga al emisor a conocer al receptor y su tarea; un evento (se ha hecho un pedido) permite al emisor declarar un hecho y a cualquier número de receptores reaccionar, así que se pueden añadir consumidores nuevos sin cambiar al productor. Propiedad de los datos. Cada dato tiene un único propietario que lo escribe; los demás módulos se lo piden al propietario a través de su interfaz o mantienen su propia copia alimentada por eventos, sin leer nunca las tablas del propietario (Sección 7.1).
Toda táctica tiene un precio. Los intermediarios añaden un salto y otra cosa más que operar. Los eventos renuncian a la consistencia inmediata y hacen más difícil ver en un solo lugar el flujo de un proceso de negocio. Las copias de datos pueden estar desactualizadas. La propiedad de los datos obliga a construir y versionar API que una tabla compartida te daba gratis. El compromiso merece la pena donde el cambio es frecuente o los equipos están separados, y a menudo no la merece entre dos clases que mantiene una sola persona.
La ley de Deméter: habla solo con tus amigos inmediatos
La ley de Deméter surgió del proyecto Demeter de la Northeastern University, donde Ian Holland identificó la regla en 1987; Karl Lieberherr, Holland y A. Riel la publicaron en la conferencia OOPSLA en 1988. Su forma general es que cada unidad solo debe tener un conocimiento limitado de otras unidades, aquellas estrechamente relacionadas con ella: "habla solo con tus amigos inmediatos". En términos de orientación a objetos, un método puede enviar mensajes a su propio objeto, a sus argumentos, a los objetos que crea y a las partes inmediatas del objeto, pero no a desconocidos a los que se llega a través de ellos.
Piensa en una rutina de asignación de repartidores de la plataforma de reparto de comida que contiene order.getCustomer().getAddress().getPostcode(). Necesita una sola cosa, un código postal, pero ahora sabe que un pedido tiene un cliente, que un cliente tiene una dirección y que una dirección tiene un código postal. Cada uno de esos hechos es una dependencia. Si los clientes pasan a tener varias direcciones guardadas, se rompe código que no tiene nada que ver con los clientes. Escrita como order.deliveryPostcode(), la misma llamada depende de una sola operación, y la estructura que hay detrás puede cambiar libremente. Un principio relacionado es "ordena, no preguntes" (tell, don't ask): en lugar de sacar datos de un objeto para actuar sobre ellos, pide al objeto que haga el trabajo. En los términos de la Sección 3.6, la cadena larga difunde el conocimiento de la estructura muy lejos de donde le corresponde estar.
La idea se puede aplicar a mayor escala. Un cliente que sigue una cadena de llamadas (obtener el pedido, luego su restaurante y luego la cuenta de liquidación de ese restaurante) depende de cómo se relacionan tres servicios. Darle una sola operación para lo que necesita, quizá mediante un backend for frontend, elimina ese conocimiento, como recomiendan las interfaces estrechas de más arriba.
El nombre la exagera. Martin Fowler ha dicho que estaría más contento si se llamara la sugerencia de Deméter, y advierte contra tomar "ordena, no preguntes" como motivo para eliminar todos los métodos de consulta. Contar puntos es una mala prueba: una cadena sobre datos simples, o un builder fluido que sigue devolviendo el mismo objeto, no revela nada sobre colaboradores ocultos. Y aplicada a ciegas, la regla llena las clases de métodos de reenvío triviales, lo que ensancha sus interfaces. Úsala como una señal para preguntarte por qué este código necesita saber tanto.
Composición en lugar de herencia
En 1994, Gamma, Helm, Johnson y Vlissides lo convirtieron en el segundo principio de Design Patterns: "Prefiere la composición de objetos a la herencia de clases". Su razonamiento se basaba en la visibilidad. La herencia es reutilización de caja blanca: la subclase ve los detalles de su clase padre y depende de ellos. La composición es reutilización de caja negra: un objeto usa otro solo a través de su interfaz. En los términos de esta sección, la herencia es uno de los acoplamientos más fuertes que ofrece un lenguaje, y queda fijada al escribir el código, así que no puede cambiarse mientras el programa se ejecuta. Un cambio en una clase padre puede romper subclases que nadie ha tocado, un problema que suele llamarse clase base frágil.
La plataforma de reparto de comida muestra cómo sale mal. Un equipo modela ExpressOrder y ScheduledOrder como subclases de Order. Luego llegan los pedidos en grupo, y alguien necesita un pedido en grupo programado con entrega exprés. La herencia simple obliga a elegir una clase padre, y añadir clases para cada combinación crece rápidamente. Con la composición, un Order tiene una FulfilmentPolicy y una PricingRule. Cada una es una interfaz pequeña con varias implementaciones, las combinaciones no necesitan ninguna clase nueva y una política puede intercambiarse en tiempo de ejecución o sustituirse por una falsa en una prueba. Esa es la forma de la interfaz de pagos de la Sección 3.3.
El principio se ha extendido al diseño de lenguajes. Go no tiene ninguna jerarquía de tipos: los tipos satisfacen las interfaces automáticamente, y la incrustación ofrece algo análogo, pero no idéntico, a la herencia de clases. A escala arquitectónica, la misma preferencia se ve en los plug-ins detrás de una interfaz estable (Sección 5.5), en lugar de un framework que todos los equipos deban extender. Una biblioteca base compartida de la que todos los servicios deban heredar puede recrear la clase base frágil en toda una organización.
La palabra es preferir, no siempre. La herencia es razonable para una verdadera relación de subtipo que cumpla la sustitución de Liskov (Sección 3.5), sobre todo dentro de un módulo cuyos propietarios controlen ambos lados. La composición tiene sus propios costos: código de reenvío, más objetos pequeños y un comportamiento más difícil de seguir leyendo una sola clase. Pregúntate si el nuevo tipo es realmente el antiguo en todos los aspectos o si solo quiere reutilizar parte de su código. Si es lo segundo, compón.
Ejemplo práctico: refactorizar una pareja de pedidos y facturación fuertemente acoplada
El módulo Pedidos de una tienda en línea llama directamente a Facturación cada vez que se hace un pedido. Después, Facturación lee la tabla de pedidos para obtener los detalles, interpreta los códigos numéricos de la columna de estado y vuelve a calcular el IVA a partir de los precios de las líneas. El código siguiente es una versión simplificada; muestra tres de los problemas a la vez.
# orders.py
def place_order(basket, customer_id):
order_id = db.insert("orders", customer_id=customer_id, status=1,
lines=basket.lines)
billing.process(order_id, mode="invoice") # Orders waits for Billing
return order_id
# billing.py
def process(order_id, mode):
row = db.query_one("SELECT * FROM orders WHERE id = %s", order_id)
if row["status"] != 1: # 1 means 'placed', by convention
return
net = sum(l["price"] * l["qty"] for l in row["lines"])
vat = round(net * 0.2) # VAT rule duplicated here
if mode == "invoice":
db.insert("invoices", order_id=order_id, net=net, vat=vat)
elif mode == "credit_note":
db.insert("credit_notes", order_id=order_id, net=-net, vat=-vat)Cualquier cambio en la estructura de la tabla de pedidos, en los códigos de estado o en las reglas del IVA tiene que hacerse ahora en los dos módulos a la vez. Pedidos no puede aceptar un pedido si Facturación va lenta o está fallando. Y los dos comportamientos de Facturación se seleccionan con un indicador que Pedidos tiene que conocer. La refactorización se hace en cuatro pasos: Pedidos deja de llamar a Facturación y, en su lugar, publica un evento OrderPlaced con exactamente los datos que necesita Facturación, incluidos los totales que Pedidos ya ha calculado; Facturación se suscribe y registra las facturas en su propio almacén; los dos comportamientos se convierten en dos operaciones; y la tabla de pedidos pasa a ser privada de Pedidos.
# orders.py
def place_order(basket, customer_id):
order = Order.create(customer_id, basket) # computes net and VAT once
repository.save(order)
events.publish("OrderPlaced", {
"schema_version": 1, "order_id": order.id,
"customer_id": customer_id,
"net_pence": order.net_pence, "vat_pence": order.vat_pence})
return order.id
# billing.py
@events.subscribe("OrderPlaced")
def on_order_placed(event):
create_invoice(event["order_id"], event["net_pence"], event["vat_pence"])
def create_invoice(order_id, net_pence, vat_pence): ...
def create_credit_note(order_id, net_pence, vat_pence): ...La pareja de pedidos y facturación, antes y después
Ejemplo práctico
Qué cambió, medido
Comparación de las dos versiones con el vocabulario de acoplamiento de esta sección.
| Tipo de acoplamiento | Antes: casi de contenido y común (tabla compartida), de control (indicador de modo), semántico (estado 1, regla del IVA). Después: acoplamiento de datos mediante un evento versionado de cinco campos. |
|---|---|
| Acoplamiento eferente de Pedidos | Antes: Ce incluye Facturación. Después: Pedidos depende solo del contrato del evento y del broker; no sabe que Facturación existe. |
| Acoplamiento temporal | Antes: un pedido falla si Facturación está caída. Después: el evento espera en el broker hasta que Facturación se recupera. |
| Acoplamiento de despliegue | Antes: un cambio en la tabla de pedidos necesita un lanzamiento conjunto. Después: Pedidos puede cambiar su tabla libremente; solo los cambios en el evento requieren cuidado, y el versionado se encarga de ellos. |
| Costos nuevos | Un broker que operar; las facturas aparecen unos segundos después en lugar de al instante; el esquema del evento es ahora un contrato público que hay que versionar; el proceso es más difícil de rastrear sin una buena observabilidad. |
El evento lleva los totales que calculó Pedidos, así que el IVA se calcula en un solo lugar. Facturación ya no necesita conocer las reglas fiscales ni la estructura de las líneas de pedido.
La refactorización eliminó una elección, no todo el acoplamiento: Facturación sigue dependiendo del significado de OrderPlaced. Esa dependencia es ahora explícita, estrecha y versionada, que es lo que significa en la práctica el acoplamiento débil.
Cómo usan los profesionales estas ideas
Los diseñadores con experiencia rara vez citan los nombres de los niveles en las revisiones, pero los aplican constantemente: cuestionan cada parámetro que actúa como indicador, buscan tablas compartidas y estado global, preguntan cuál es el único propósito de cada módulo y observan qué módulos cambian juntos en el control de versiones. En los límites entre equipos y servicios son estrictos, porque allí el acoplamiento es caro de deshacer; dentro de un módulo pequeño del que es propietaria una sola persona son flexibles, porque allí es barato.
También hacen visible el acoplamiento. Un grafo de dependencias generado a partir del código, un recuento de Ca y Ce por componente y una lista de las tablas de base de datos a las que accede más de un servicio están entre las vistas más informativas de una arquitectura, y todas pueden generarse automáticamente y seguirse en la compilación. La siguiente sección pasa al principio que mantiene débil el acoplamiento desde el principio: ocultar las decisiones tras interfaces.
Comprueba lo que has entendido
¿Por qué una función con un parámetro de modo que selecciona comportamientos completamente distintos es un ejemplo de acoplamiento de control?
Porque quien llama tiene que saber cómo está organizado por dentro quien es llamado y elegir su rama interna; cambiar la lógica de quien es llamado puede obligar a cambiar a quienes lo llaman.
Un módulo tiene Ca = 12 y Ce = 1. ¿Qué sugiere esto?
Muchos módulos dependen de él y él depende de poco, así que cambiarlo es arriesgado y los cambios deben ser escasos y cuidadosos; su interfaz merece una estabilidad especial.
Dos servicios se comunican solo mediante eventos, pero siempre deben lanzarse juntos. ¿Qué tipo de acoplamiento queda?
El acoplamiento de despliegue, normalmente causado por cambios no compatibles con versiones anteriores en el evento compartido o por acuerdos semánticos ocultos.
Indica un costo de sustituir una llamada directa por un evento.
Los resultados pasan a tener consistencia eventual en lugar de inmediata; además, ganas un broker que operar, un esquema de eventos que versionar y un flujo más difícil de rastrear.
Inténtalo: Booking depende de Seats, Payments y Email. Refunds depende de Payments y Email. Reports depende de Seats y Payments. Calcula Ca y Ce de Payments y Booking. ¿Cuál es más arriesgado cambiar y cuál está más expuesto?
Payments: Ca = 3 (Booking, Refunds, Reports), Ce = 0. Booking: Ca = 0, Ce = 3 (Seats, Payments, Email). Payments es más arriesgado de cambiar, porque tres módulos notan cualquier cambio en su interfaz, así que mantén esa interfaz estrecha y estable. Booking está más expuesto, ya que un cambio en cualquiera de sus tres dependencias puede romperlo, pero cambiar el propio Booking es seguro, porque nada depende de él. Ese reparto del riesgo es saludable.
Fuentes y lecturas recomendadas
- Martin Fowler, Reducing Coupling (IEEE Software, 2001)
- W. P. Stevens, G. J. Myers and L. L. Constantine, Structured Design (IBM Systems Journal, 1974)
- Microservices: database per service pattern (microservices.io)
- Martin Fowler, What do you mean by Event-Driven?
- Law of Demeter: general formulation (Northeastern University)
- Law of Demeter: object formulation (Northeastern University)
- Object-oriented programming: an objective sense of style (record)
- Law of Demeter (Wikipedia, for authors and project)
- Fowler: Mocks Aren't Stubs
- Fowler: TellDontAsk
- When did people favor composition over inheritance?
- The Go FAQ
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “Nuestros dos servicios comparten una tabla de base de datos. ¿Cómo los separo paso a paso sin una migración de golpe (big bang)?”