Twin Pattern
Link two classes through mutual references so they delegate methods to each other: a composition-based alternative to multiple inheritance.
Overview
Java and C# don’t have multiple inheritance, and mixins aren’t always an option. When a class genuinely needs behavior from two unrelated hierarchies, say a widget that has to both draw itself and handle events, the usual answers are awkward: a fat interface, a god object, or duplicated glue code.
The Twin Pattern splits that class into two sibling classes linked by mutual references. A Widget keeps the shared state and the public API, a Graphic twin owns drawing, an Interactive twin owns events, and each twin holds a reference back to the widget. Calls that belong to one domain get forwarded to the twin that owns it.
The payoff: each twin can evolve, get tested, and be swapped independently, while clients still see one object.
When to Use
Reach for the Twin Pattern when all of these hold:
- A class needs behavior from multiple orthogonal hierarchies
- The target language doesn’t support multiple inheritance
- Mixins or traits are unavailable or can’t express what you need
- Two aspects of a class should evolve independently with minimal coupling
When to Avoid
- The class collapses into a single hierarchy once you introduce strategy objects
- Multiple inheritance or mixins are available and cleaner; the Mixin Pattern covers the same ground with less wiring
- The twins end up in circular call loops that nobody can follow
- Plain composition with one-way delegation is enough
- The behaviors don’t share state at all; independent objects tagged with a Marker Interface or split via the Role Pattern may be simpler
Solution
Python
from abc import ABC, abstractmethod
from typing import Optional
class Graphic:
"""Abstract base for drawing behavior"""
def __init__(self):
self.widget: Optional['Widget'] = None
def draw(self):
print(f"Drawing {self.widget.name} at ({self.widget.x}, {self.widget.y})")
def resize(self, width: int, height: int):
self.widget.width = width
self.widget.height = height
print(f"Resized to {width}x{height}")
class Interactive:
"""Abstract base for interaction behavior"""
def __init__(self):
self.widget: Optional['Widget'] = None
def on_click(self):
print(f"Clicked on {self.widget.name}")
def on_hover(self):
print(f"Hovering over {self.widget.name}")
class Widget:
"""The twin class that links Graphic and 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
# Create twins and link them
self._graphic = Graphic()
self._graphic.widget = self
self._interactive = Interactive()
self._interactive.widget = self
# Delegate drawing to Graphic twin
def draw(self):
self._graphic.draw()
def resize(self, width: int, height: int):
self._graphic.resize(width, height)
# Delegate interaction to Interactive twin
def on_click(self):
self._interactive.on_click()
def on_hover(self):
self._interactive.on_hover()
# Cross-twin access
def get_graphic(self) -> Graphic:
return self._graphic
def get_interactive(self) -> Interactive:
return self._interactive
# Usage
button = Widget("SubmitButton", 10, 20)
button.draw() # Delegated to Graphic twin
button.on_click() # Delegated to Interactive twin
button.resize(200, 60)
Java
// Twin A: Drawing behavior
class Graphic {
private Widget widget;
public void setWidget(Widget widget) { this.widget = widget; }
public void draw() {
System.out.println("Drawing " + widget.getName() + " at (" + widget.getX() + ", " + widget.getY() + ")");
}
public void resize(int width, int height) {
widget.setWidth(width);
widget.setHeight(height);
System.out.println("Resized to " + width + "x" + height);
}
}
// Twin B: Interaction behavior
class Interactive {
private Widget widget;
public void setWidget(Widget widget) { this.widget = widget; }
public void onClick() {
System.out.println("Clicked on " + widget.getName());
}
public void onHover() {
System.out.println("Hovering over " + widget.getName());
}
}
// The composite twin class
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; }
// Delegation methods
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; }
}
// Usage
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(`Drawing ${this.widget.name} at (${this.widget.x}, ${this.widget.y})`);
}
resize(width, height) {
this.widget.width = width;
this.widget.height = height;
console.log(`Resized to ${width}x${height}`);
}
}
class Interactive {
constructor() {
this.widget = null;
}
onClick() {
console.log(`Clicked on ${this.widget.name}`);
}
onHover() {
console.log(`Hovering over ${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;
}
}
// Usage
const button = new Widget('SubmitButton', 10, 20);
button.draw();
button.onClick();
button.resize(200, 60);
Explanation
The mechanics are composition with a twist: the composed objects point back.
- Widget is the class clients call. It owns the shared state (name, position, size).
- Graphic and Interactive are the twins. Each owns exactly one concern.
- Both twins store a back-reference to the widget, wired when they’re created. That’s what makes them twins instead of plain components.
Widgetforwards each call to the right twin:draw()goes to Graphic,onClick()goes to Interactive.
The back-reference is the whole trick. Without it, Graphic couldn’t see the widget’s position, and you’d pass state through every method call, precisely the plumbing this pattern removes.
Variants
| Variant | Structure | Use Case |
|---|---|---|
| Simple twin | Two linked twins | Drawing + interaction |
| Multi-twin | Three or more linked twins | Complex widgets with layout, style, events |
| Twin with interface | Both twins implement the same interface | Interchangeable twins |
| Twin factory | A factory creates and links the twins | UI toolkits |
What Works
- Keep the public class thin. The widget delegates; the logic lives in the twins.
- Avoid circular logic. Twins shouldn’t call each other’s methods in loops.
- Make twins replaceable. Swap one twin without recreating the widget.
- Use interfaces for twins. In typed languages, define
IGraphicandIInteractiveso the twins are mockable in tests. - Consider the observer pattern for cross-twin communication. Events beat direct calls between twins.
Common Mistakes
- Tight coupling between twins. Twins interact through the widget, never directly.
- Exposing twins publicly. Clients talk to the widget, not to the twins.
- Duplicate state. State lives in the widget; don’t mirror it inside twins.
- Forgetting to link twins. A twin with a null widget reference throws on the first delegated call. Link them in the constructor, not later.
- Out-engineering multiple inheritance. If your language has mixins, use them.
Real-World Examples
UI Frameworks
Java’s AWT/Swing separates Component (the widget) from ComponentPeer (the native twin). The peer handles platform-specific rendering and events.
Game Engines
Unity’s Entity-Component-System separates data (components) from behavior (systems). Not a strict twin, but the split mirrors the pattern’s intent.
ORM Proxies
Hibernate’s proxy objects split an entity into a proxy twin (lazy loading) and a target twin (the actual data). The proxy delegates to the target once initialized.
Testing Twins in Isolation
The back-reference looks like it makes twins untestable, but each twin only reads a handful of fields. Give the twin a small interface and inject a stub instead of a real widget:
class FakeWidget:
name = "Stub"
x = y = 0
width = height = 0
graphic = Graphic()
graphic.widget = FakeWidget()
assert graphic.draw() == "Drawing Stub at (0, 0)"
The companion repo ships a fuller version of this: twin_widget.py includes a unittest suite covering delegation, stub-based twin isolation, twin swapping, and the fail-fast error when a twin was never linked.
Frequently Asked Questions
What's the difference between the Twin Pattern and Bridge?
Bridge separates an abstraction hierarchy from an implementation hierarchy so both can vary independently. Twin splits a single class into two cooperating parts that share state through back-references. Different problem: Bridge is about independent hierarchies, Twin is about decomposing one class.
Is the Twin Pattern just composition?
Yes, with one extra rule: the composed objects hold a reference back to the composing object. That back-reference is what turns plain composition into twins: each part can read shared state without it being passed through every call.
Can I use more than two twins?
Yes. Three or more twins form a hub-and-spoke arrangement around the main class, which stays the only owner of shared state. Complexity grows with each twin, so keep the count low.
How do I test a twin in isolation?
Give the twin a minimal interface for what it needs from the widget and inject a stub. In the examples above, Graphic only reads name, x, y, width, height, so a fake widget exposing those fields is enough to unit-test draw() and resize(). It works the other way too: test the widget with mock twins.
Doesn't the mutual reference create a circular dependency?
It creates a cycle between objects, but it's a data dependency, not a compile-time one. In garbage-collected languages it's harmless. In C++ or Rust, make the back-reference weak (weak_ptr, Rc<Weak>) so the cycle can't leak.
When should I prefer mixins or traits instead?
Whenever your language has them and the behaviors don't need an independent lifecycle. Twin pays off when each twin needs its own state, its own test harness, or runtime swapping, like switching a platform-specific render twin.
Related Resources
Bridge Pattern: Decouple Abstraction from Implementation
Split a class into two hierarchies — abstraction and implementation — so both can evolve independently. Includes Python, Java, and JavaScript examples.
PatternDecorator Pattern
Add new functionality to objects dynamically by wrapping them. A structural design pattern for flexible behavior extension.
PatternStrategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
PatternMarker Interface Pattern
Use empty interfaces as metadata tags to signal properties or capabilities at compile time and runtime, enabling type-safe checks without modifying class behavior.
PatternPartial Class Pattern
Split a single class across two or more files so generated and hand-written code can coexist without overwriting each other.
PatternRole Pattern
Assign dynamic roles to objects at runtime instead of hard-coding behavior in class hierarchies, enabling flexible identity changes without inheritance bloat.