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.
Overview
El patrón Bridge es un patrón de diseño estructural que separa una abstracción de su implementación. En lugar de una sola jerarquía que mezcla ambas, tenés dos: una para la abstracción y otra para la implementación. Eso permite que cada lado evolucione por su cuenta sin romper el otro lado.
Recurrí a Bridge en proyectos donde un framework de UI tenía que soportar backends Canvas y WebGL
across una docena de tipos de widgets. Sin Bridge, tendríamos CanvasButton, WebGLButton,
CanvasSlider, WebGLSlider — una explosión de clases. Con Bridge, cada widget mantenía una
referencia al renderer y le delegaba el dibujo. Agregar un renderer nuevo significaba una clase
nueva, no una docena.
Un ejemplo típico: una UI con distintos widgets y distintos backends de renderizado. Sin Bridge,
cada clase de widget tendría que conocer cada renderizador. Con Bridge, un Circle mantiene una
referencia a un Renderer y le delega el dibujo. Podés agregar formas o renderizadores después sin
romper nada que ya funcione. Para un enfoque relacionado sobre adaptar interfaces, mirá nuestro
patrón Adapter — el sibling con el que más confunden a Bridge.
Cuándo Usarlo
- Querés evitar un enlace permanente entre la abstracción y su implementación.
- Ambos lados del diseño deben ser extensibles mediante subclases.
- Varios objetos necesitan compartir la misma implementación subyacente.
- Los cambios en la implementación no deberían llegar a los clientes.
- Tenés una explosión de clases porque dos dimensiones, como formas y renderizadores, se combinan en una sola jerarquía.
Cuándo NO Usarlo
- Un simple Strategy o Adapter ya cubre una sola dimensión de variación.
- El proyecto es pequeño y la jerarquía extra agrega más complejidad que valor.
- No controlás ninguno de los dos lados de la abstracción/implementación.
Solución
Python
from abc import ABC, abstractmethod
class Renderer(ABC):
@abstractmethod
def render_circle(self, radius: float):
pass
class VectorRenderer(Renderer):
def render_circle(self, radius: float):
print(f"Drawing a circle of radius {radius} with vector graphics")
class RasterRenderer(Renderer):
def render_circle(self, radius: float):
print(f"Drawing pixels for a circle of radius {radius}")
class Shape(ABC):
def __init__(self, renderer: Renderer):
self.renderer = renderer
@abstractmethod
def draw(self):
pass
class Circle(Shape):
def __init__(self, renderer: Renderer, radius: float):
super().__init__(renderer)
self.radius = radius
def draw(self):
self.renderer.render_circle(self.radius)
circle_vector = Circle(VectorRenderer(), 5.0)
circle_vector.draw()
circle_raster = Circle(RasterRenderer(), 10.0)
circle_raster.draw()
JavaScript
class VectorRenderer {
renderCircle(radius) {
console.log(`Drawing a circle of radius ${radius} with vector graphics`);
}
}
class RasterRenderer {
renderCircle(radius) {
console.log(`Drawing pixels for a circle of radius ${radius}`);
}
}
class Shape {
constructor(renderer) {
this.renderer = renderer;
}
draw() {
throw new Error("Subclasses must implement draw()");
}
}
class Circle extends Shape {
constructor(renderer, radius) {
super(renderer);
this.radius = radius;
}
draw() {
this.renderer.renderCircle(this.radius);
}
}
const cv = new Circle(new VectorRenderer(), 5);
cv.draw();
const cr = new Circle(new RasterRenderer(), 10);
cr.draw();
Java
public interface Renderer {
void renderCircle(double radius);
}
public class VectorRenderer implements Renderer {
public void renderCircle(double radius) {
System.out.println("Drawing a circle of radius " + radius + " with vector graphics");
}
}
public class RasterRenderer implements Renderer {
public void renderCircle(double radius) {
System.out.println("Drawing pixels for a circle of radius " + radius);
}
}
public abstract class Shape {
protected final Renderer renderer;
public Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
public class Circle extends Shape {
private final double radius;
public Circle(Renderer renderer, double radius) {
super(renderer);
this.radius = radius;
}
public void draw() {
renderer.renderCircle(radius);
}
}
Shape cv = new Circle(new VectorRenderer(), 5.0);
cv.draw();
Explicación
El patrón separa dos dimensiones en dos jerarquías de clases:
- Abstracción (
Shape): la interfaz de alto nivel que usan los clientes. - Implementación (
Renderer): las operaciones de bajo nivel que hacen el trabajo.
La abstracción mantiene una referencia a la implementación y le delega el trabajo. Esa separación permite agregar nuevas formas o nuevos renderizadores sin tocar código que ya funciona.
Variantes
| Variante | Descripción | Caso de uso |
|---|---|---|
| Bridge clásico | Dos jerarquías paralelas | Formas y renderizadores, dispositivos y drivers |
| Driver Bridge | Abstracción sobre APIs de hardware o SO | Frameworks de UI cross-platform |
| Remote Bridge | Abstracción local sobre implementación remota | Stubs y proxies RPC |
Renderizado cross-platform en TypeScript
interface Renderer {
renderCircle(x: number, y: number, r: number): string;
renderRect(x: number, y: number, w: number, h: number): string;
}
class SVGRenderer implements Renderer {
renderCircle(x, y, r) { return `<circle cx="${x}" cy="${y}" r="${r}" />`; }
renderRect(x, y, w, h) { return `<rect x="${x}" y="${y}" width="${w}" height="${h}" />`; }
}
class CanvasRenderer implements Renderer {
renderCircle(x, y, r) { return `ctx.arc(${x}, ${y}, ${r}, 0, Math.PI * 2); ctx.stroke();`; }
renderRect(x, y, w, h) { return `ctx.strokeRect(${x}, ${y}, ${w}, ${h});`; }
}
abstract class Shape {
constructor(protected renderer: Renderer) {}
abstract draw(): string;
}
class Circle extends Shape {
constructor(renderer: Renderer, private x: number, private y: number, private r: number) {
super(renderer);
}
draw() { return this.renderer.renderCircle(this.x, this.y, this.r); }
}
const svgCircle = new Circle(new SVGRenderer(), 50, 50, 20);
const canvasCircle = new Circle(new CanvasRenderer(), 50, 50, 20);
console.log(svgCircle.draw()); // SVG circle
console.log(canvasCircle.draw()); // Canvas circle
Remote Bridge con un proxy local
Remote Bridge pone la implementación del otro lado de una llamada de red. La abstracción vive localmente y le delega a un proxy que maneja el protocolo de red. Recurrí a este patrón para wrappear servicios gRPC así el código de la aplicación puede llamarlos como si fueran objetos locales.
import json
import urllib.request
class RemoteRenderer:
"""Abstracción local sobre un servicio de renderizado remoto."""
def __init__(self, endpoint: str):
self.endpoint = endpoint
def render_circle(self, radius: float):
payload = json.dumps({"shape": "circle", "radius": radius}).encode()
req = urllib.request.Request(
f"{self.endpoint}/render",
data=payload,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
return resp.read().decode()
# El código de la aplicación no sabe que el renderer es remoto
remote = RemoteRenderer("https://render.example.com")
circle = Circle(remote, 7.5)
circle.draw() # Llama al servicio remoto transparentemente
Lo lindo acá es que Circle no cambia nada — sigue llamando
self.renderer.render_circle(self.radius). La complejidad remota está escondida dentro de la
implementación, no en la abstracción.
Buenas Prácticas
- Identificá las dimensiones independientes antes de aplicar el patrón. No todo problema de jerarquías necesita un Bridge.
- Mantené la interfaz de implementación mínima. Exponé solo lo que la abstracción necesita.
- Preferí composición sobre herencia. El patrón Bridge se basa en composición.
- Usá inyección de dependencias para conectar implementaciones con abstracciones.
- Documentá qué lado es la abstracción y cuál la implementación.
Errores Comunes
- Aplicar Bridge cuando un simple Strategy o Adapter alcanza.
- Hacer la interfaz de implementación demasiado amplia y acoplarla a la abstracción.
- Dejar que la abstracción filtre detalles de implementación a los clientes.
- Crear jerarquías profundas en ambos lados y reintroducir la complejidad que el patrón evitaba.
See Also
- Refactoring.Guru — Patrón Bridge — una explicación visual y beginner-friendly con analogías y un class diagram.
- Wikipedia — Patrón Bridge — la referencia clásica con contexto GoF y ejemplos adicionales.
- Libro GoF Design Patterns — la fuente original del patrón Bridge y otros 22 patrones estructurales, creacionales y de comportamiento.
- Patrón Strategy — nuestro deep dive sobre Strategy, el patrón que más se confunde con Bridge.
- Patrón Adapter — nuestra guía de Adapter, el otro patrón que la gente confunde con Bridge.
Preguntas frecuentes
¿Cuál es la diferencia entre Bridge y Adapter?
Adapter hace que interfaces incompatibles trabajen juntas. Bridge separa una abstracción de su implementación para que ambas evolucionen independientemente.
¿Cuándo uso Bridge en vez de Strategy?
Strategy varía un solo algoritmo. Bridge separa dos jerarquías completas de clases. Usá Bridge cuando tengas dos dimensiones de variación independientes.
¿Es adecuado para proyectos pequeños?
En proyectos pequeños con pocos componentes, Bridge puede agregar más complejidad de la que vale. Empezá simple y sacalo cuando el dolor se vuelva lo suficientemente real.
¿Puedo aplicar el patrón parcialmente?
Sí. Muchos equipos adoptan patrones de a poco. Empezá con la idea central y agregá sofisticación solo donde sea necesaria. El patrón es una guía, no un plano.
¿Por qué Bridge se confunde con Strategy?
Ambos usan composición para delegar trabajo. La diferencia es el scope: Strategy swappea un algoritmo dentro de una sola clase, mientras que Bridge separa dos jerarquías completas. Si estás variando una sola cosa, usá Strategy. Si estás variando dos cosas independientes, Bridge es la herramienta correcta.
¿Funciona Bridge con frameworks de inyección de dependencias?
Sí, y se lleva bien con ellos. Spring, Dagger y containers de DI similares pueden wirear la implementación en la abstracción en runtime. Eso convierte swappear renderers o backends en un ajuste de config, no un refactor.
Recursos Relacionados
Patrón Adapter
Convierte la interfaz de una clase en otra interfaz que los clientes esperan. Patrón de diseño estructural para compatibilidad de interfaces.
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 Twin
Provee una alternativa a la herencia múltiple vinculando dos clases separadas a través de referencias mutuas, permitiéndoles delegar métodos entre sí según sea necesario.
PatternPatron Factory
Crea objetos sin especificar la clase exacta a instanciar. Un patrón de diseño creacional para la creación flexible de objetos.
PatternPatrón Singleton
Garantiza que una clase tenga una única instancia y proporciona un acceso global a ella. Patrón de diseño creacional para controlar la creación de objetos.