Patrón Twin
Vincula dos clases mediante referencias mutuas para que deleguen métodos entre sí: una alternativa a la herencia múltiple basada en composición.
Descripción General
Java y C# no tienen herencia múltiple, y los mixins no siempre son una opción. Cuando una clase necesita de verdad comportamiento de dos jerarquías sin relación entre sí, por ejemplo un widget que tiene que dibujarse y además manejar eventos, las respuestas habituales son incómodas: una interfaz gigante, un objeto todopoderoso o código de pegamento duplicado.
El Patrón Twin divide esa clase en dos clases hermanas vinculadas por referencias mutuas. Un Widget guarda el estado compartido y la API pública, un twin Graphic se encarga del dibujo, un twin Interactive se encarga de los eventos, y cada twin guarda una referencia de vuelta al widget. Cuando llega una llamada que pertenece a un dominio, el widget se la pasa al twin que la posee.
¿Para qué? Cada twin puede evolucionar, testearse e intercambiarse por su cuenta, mientras los clientes siguen viendo un único objeto.
Cuándo Usar
Usa el Patrón Twin cuando se cumplan todas estas condiciones:
- Una clase necesita comportamiento de múltiples jerarquías ortogonales
- El lenguaje objetivo no soporta herencia múltiple
- Los mixins o traits no están disponibles o no expresan lo que necesitas
- Dos aspectos de una clase deberían evolucionar de forma independiente con mínimo acoplamiento
Cuándo Evitar
- La clase se simplifica en una única jerarquía al introducir objetos strategy
- La herencia múltiple o los mixins están disponibles y son más limpios; el Patrón Mixin cubre el mismo terreno con menos cableado
- Los twins acaban en bucles de llamadas circulares que nadie puede seguir
- Una composición simple con delegación unidireccional es suficiente
- Los comportamientos no comparten estado en absoluto; objetos independientes etiquetados con una Interfaz Marcador o divididos con el Patrón Role pueden ser más simples
Solución
Python
from typing import Optional
class Graphic:
"""Base abstracta para el comportamiento de dibujo"""
def __init__(self):
self.widget: Optional['Widget'] = None
def draw(self):
print(f"Dibujando {self.widget.name} en ({self.widget.x}, {self.widget.y})")
def resize(self, width: int, height: int):
self.widget.width = width
self.widget.height = height
print(f"Redimensionado a {width}x{height}")
class Interactive:
"""Base abstracta para el comportamiento de interacción"""
def __init__(self):
self.widget: Optional['Widget'] = None
def on_click(self):
print(f"Click en {self.widget.name}")
def on_hover(self):
print(f"Hover sobre {self.widget.name}")
class Widget:
"""La clase twin que vincula Graphic e Interactive"""
def __init__(self, name: str, x: int = 0, y: int = 0):
self.name = name
self.x = x
self.y = y
self.width = 100
self.height = 50
# Crear twins y vincularlos
self._graphic = Graphic()
self._graphic.widget = self
self._interactive = Interactive()
self._interactive.widget = self
# Delegar dibujo al twin Graphic
def draw(self):
self._graphic.draw()
def resize(self, width: int, height: int):
self._graphic.resize(width, height)
# Delegar interacción al twin Interactive
def on_click(self):
self._interactive.on_click()
def on_hover(self):
self._interactive.on_hover()
# Acceso entre twins
def get_graphic(self) -> Graphic:
return self._graphic
def get_interactive(self) -> Interactive:
return self._interactive
# Uso
button = Widget("SubmitButton", 10, 20)
button.draw() # Delegado al twin Graphic
button.on_click() # Delegado al twin Interactive
button.resize(200, 60)
Java
// Twin A: comportamiento de dibujo
class Graphic {
private Widget widget;
public void setWidget(Widget widget) { this.widget = widget; }
public void draw() {
System.out.println("Dibujando " + widget.getName() + " en (" + widget.getX() + ", " + widget.getY() + ")");
}
public void resize(int width, int height) {
widget.setWidth(width);
widget.setHeight(height);
System.out.println("Redimensionado a " + width + "x" + height);
}
}
// Twin B: comportamiento de interacción
class Interactive {
private Widget widget;
public void setWidget(Widget widget) { this.widget = widget; }
public void onClick() {
System.out.println("Click en " + widget.getName());
}
public void onHover() {
System.out.println("Hover sobre " + widget.getName());
}
}
// La clase twin compuesta
class Widget {
private final String name;
private int x, y, width, height;
private final Graphic graphic = new Graphic();
private final Interactive interactive = new Interactive();
public Widget(String name, int x, int y) {
this.name = name; this.x = x; this.y = y;
this.width = 100; this.height = 50;
graphic.setWidget(this);
interactive.setWidget(this);
}
public String getName() { return name; }
public int getX() { return x; }
public int getY() { return y; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
// Métodos de delegación
public void draw() { graphic.draw(); }
public void resize(int w, int h) { graphic.resize(w, h); }
public void onClick() { interactive.onClick(); }
public void onHover() { interactive.onHover(); }
public Graphic getGraphic() { return graphic; }
public Interactive getInteractive() { return interactive; }
}
// Uso
Widget button = new Widget("SubmitButton", 10, 20);
button.draw();
button.onClick();
button.resize(200, 60);
JavaScript
class Graphic {
constructor() {
this.widget = null;
}
draw() {
console.log(`Dibujando ${this.widget.name} en (${this.widget.x}, ${this.widget.y})`);
}
resize(width, height) {
this.widget.width = width;
this.widget.height = height;
console.log(`Redimensionado a ${width}x${height}`);
}
}
class Interactive {
constructor() {
this.widget = null;
}
onClick() {
console.log(`Click en ${this.widget.name}`);
}
onHover() {
console.log(`Hover sobre ${this.widget.name}`);
}
}
class Widget {
constructor(name, x = 0, y = 0) {
this.name = name;
this.x = x;
this.y = y;
this.width = 100;
this.height = 50;
this.graphic = new Graphic();
this.graphic.widget = this;
this.interactive = new Interactive();
this.interactive.widget = this;
}
draw() {
this.graphic.draw();
}
resize(width, height) {
this.graphic.resize(width, height);
}
onClick() {
this.interactive.onClick();
}
onHover() {
this.interactive.onHover();
}
getGraphic() {
return this.graphic;
}
getInteractive() {
return this.interactive;
}
}
// Uso
const button = new Widget('SubmitButton', 10, 20);
button.draw();
button.onClick();
button.resize(200, 60);
Explicación
La mecánica es composición con un giro: los objetos compuestos también apuntan hacia atrás, hacia quien los contiene.
- Widget es la clase que llaman los clientes. Posee el estado compartido (nombre, posición, tamaño).
- Graphic e Interactive son los twins. Cada uno posee exactamente una responsabilidad.
- Ambos twins guardan una referencia de vuelta al widget, que se conecta al crearlos. Eso es lo que los convierte en twins y no en simples componentes.
Widgetreenvía cada llamada al twin correcto:draw()va a Graphic,onClick()va a Interactive.
La referencia de vuelta es todo el truco. Sin ella, Graphic no podría ver la posición del widget, y acabarías pasando el estado en cada llamada a método, que es justo la fontanería que este patrón viene a eliminar.
Variantes
| Variante | Estructura | Caso de Uso |
|---|---|---|
| Twin simple | Dos twins vinculados | Dibujo + interacción |
| Multi-twin | Tres o más twins vinculados | Widgets complejos con layout, estilo, eventos |
| Twin con interfaz | Ambos twins implementan la misma interfaz | Twins intercambiables |
| Twin factory | Una factory crea y vincula los twins | Toolkits de UI |
Lo que Funciona
- Mantén la clase pública ligera. El widget delega; la lógica vive en los twins.
- Evita la lógica circular. Los twins no deberían llamarse entre sí en bucles.
- Haz los twins reemplazables. Intercambia un twin sin recrear el widget.
- Usa interfaces para los twins. En lenguajes tipados, define
IGraphiceIInteractivepara poder mockear los twins en los tests. - Considera el patrón observer para la comunicación entre twins. Los eventos ganan a las llamadas directas entre twins.
Errores Comunes
- Acoplamiento fuerte entre twins. Los twins interactúan a través del widget, nunca directamente.
- Exponer los twins públicamente. Los clientes hablan con el widget, no con los twins.
- Estado duplicado. El estado vive en el widget; no lo reflejes dentro de los twins.
- Olvidar vincular los twins. Un twin con una referencia nula al widget lanza un error en la primera llamada delegada. Vincúlalos en el constructor, no después.
- Sobre-ingeniería frente a la herencia múltiple. Si tu lenguaje tiene mixins, úsalos.
Ejemplos del Mundo Real
Frameworks de UI
El AWT/Swing de Java separa Component (el widget) de ComponentPeer (el twin nativo). El peer maneja el renderizado y los eventos específicos de la plataforma.
Motores de Juego
El Entity-Component-System de Unity separa los datos (componentes) del comportamiento (sistemas). No es un twin estricto, pero la división refleja la intención del patrón.
Proxies de ORM
Los objetos proxy de Hibernate dividen una entidad en un twin proxy (carga perezosa) y un twin destino (los datos reales). El proxy delega al destino una vez inicializado.
Testear los Twins de Forma Aislada
La referencia de vuelta parece hacer los twins intestables, pero cada twin solo lee un puñado de campos. Dale al twin una interfaz pequeña e inyecta un stub en lugar de un widget real:
class FakeWidget:
name = "Stub"
x = y = 0
width = height = 0
graphic = Graphic()
graphic.widget = FakeWidget()
assert graphic.draw() == "Dibujando Stub en (0, 0)"
El repositorio complementario incluye una versión más completa: twin_widget.py trae una suite unittest que cubre la delegación, el aislamiento de twins con stub, el intercambio de twins y el error fail-fast cuando un twin nunca se vinculó.
Preguntas frecuentes
¿Cuál es la diferencia entre el Patrón Twin y Bridge?
Bridge separa una jerarquía de abstracción de una jerarquía de implementación para que ambas puedan variar de forma independiente. Twin divide una única clase en dos partes cooperativas que comparten estado mediante referencias de vuelta. Son problemas distintos: Bridge trata de jerarquías independientes, Twin de descomponer una clase.
¿El Patrón Twin es solo composición?
Sí, con una regla extra: los objetos compuestos guardan una referencia de vuelta al objeto que los compone. Esa referencia es lo que convierte la composición simple en twins: cada parte puede leer el estado compartido sin que se lo pasen en cada llamada.
¿Puedo usar más de dos twins?
Sí. Tres o más twins forman una disposición en estrella alrededor de la clase principal, que sigue siendo la única dueña del estado compartido. La complejidad crece con cada twin, así que mantén el número bajo.
¿Cómo testeo un twin de forma aislada?
Dale al twin una interfaz mínima de lo que necesita del widget e inyecta un stub. En los ejemplos de arriba, Graphic solo lee name, x, y, width, height, así que un widget falso que exponga esos campos basta para testear draw() y resize() de forma unitaria. También funciona al revés: testea el widget con twins mock.
¿La referencia mutua no crea una dependencia circular?
Crea un ciclo entre objetos, pero es una dependencia de datos, no de compilación. En lenguajes con recolector de basura es inofensiva. En C++ o Rust, haz la referencia de vuelta débil (weak_ptr, Rc<Weak>) para que el ciclo no provoque fugas.
¿Cuándo debería preferir mixins o traits?
Siempre que tu lenguaje los tenga y los comportamientos no necesiten un ciclo de vida independiente. Twin compensa cuando cada twin necesita su propio estado, su propio arnés de tests o intercambio en tiempo de ejecución, como cambiar un twin de renderizado específico de plataforma.
Recursos Relacionados
Patrón Bridge: Desacopla Abstracción e Implementación
Dividí una clase en dos jerarquías — abstracción e implementación — para que ambas evolucionen independientemente. Con ejemplos en Python, Java y JavaScript.
PatternPatrón Decorator
Añade nueva funcionalidad a objetos dinámicamente envolviéndolos. Patrón de diseño estructural para extensión flexible de comportamiento.
PatternPatrón Strategy
Define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Patrón de diseño conductual para selección flexible de comportamiento.
PatternPatrón Marker Interface
Usa interfaces vacías como tags de metadata para señalar propiedades o capacidades en tiempo de compilación y runtime, habilitando verificaciones type-safe sin modificar comportamiento de clase.
PatternPatrón Partial Class
Dividí una clase en dos o más archivos para que el código generado y el escrito a mano coexistan sin sobrescribirse.
PatternPatrón Role
Asigna roles dinámicos a objetos en runtime en lugar de codificar comportamiento en jerarquías de clase, habilitando cambios flexibles de identidad sin bloat de herencia.