Skip to content
StackPractices
intermediate Por Mathias Paulenko

Patrón Bridge

Desacopla una abstracción de su implementación para que ambas puedan variar independientemente. Un patrón estructural para independencia de plataforma.

Temas: design

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Patrón Bridge

Visión General

El Patrón Bridge es un patrón de diseño estructural que desacopla una abstracción de su implementación para que ambas puedan variar independientemente. En lugar de tener una jerarquía de clases que combina ambas, divides en dos jerarquías separadas — una para la abstracción y otra para la implementación. Esto es especialmente útil cuando necesitas soportar múltiples plataformas o backends de renderizado.

Cuándo Usarlo

Usa el Patrón Bridge cuando:

  • Quieres evitar un enlace permanente entre una abstracción y su implementación
  • Tanto la abstracción como su implementación deben ser extensibles por subclasificación
  • Quieres compartir una implementación entre múltiples objetos
  • Los cambios en la implementación no deberían afectar a los clientes
  • Tienes una jerarquía de clases proliferante al combinar dimensiones (ej. formas × renderizadores)

Solución

Python

from abc import ABC, abstractmethod

# Jerarquía de implementación
class Renderer(ABC):
    @abstractmethod
    def render_circle(self, radius: float):
        pass

class VectorRenderer(Renderer):
    def render_circle(self, radius: float):
        print(f"Dibujando un círculo de radio {radius} con gráficos vectoriales")

class RasterRenderer(Renderer):
    def render_circle(self, radius: float):
        print(f"Dibujando píxeles para un círculo de radio {radius}")

# Jerarquía de abstracción
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)

# Uso: combinar cualquier forma con cualquier renderizador
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(`Dibujando un círculo de radio ${radius} con gráficos vectoriales`);
  }
}

class RasterRenderer {
  renderCircle(radius) {
    console.log(`Dibujando píxeles para un círculo de radio ${radius}`);
  }
}

class Shape {
  constructor(renderer) {
    this.renderer = renderer;
  }
  draw() {
    throw new Error("Las subclases deben implementar draw()");
  }
}

class Circle extends Shape {
  constructor(renderer, radius) {
    super(renderer);
    this.radius = radius;
  }

  draw() {
    this.renderer.renderCircle(this.radius);
  }
}

// Uso
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("Dibujando un círculo de radio " + radius + " con gráficos vectoriales");
    }
}

public class RasterRenderer implements Renderer {
    public void renderCircle(double radius) {
        System.out.println("Dibujando píxeles para un círculo de radio " + 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);
    }
}

// Uso
Shape cv = new Circle(new VectorRenderer(), 5.0);
cv.draw();

Explicación

El Patrón Bridge separa dos dimensiones en dos jerarquías de clases:

  • Abstracción (Shape): Define la interfaz de alto nivel con la que interactúan los clientes
  • Implementación (Renderer): Define las operaciones de bajo nivel que realizan el trabajo

La abstracción mantiene una referencia a la implementación y delega el trabajo a ella. Puedes agregar nuevas formas (ej. Square) o nuevos renderizadores (ej. SVGRenderer) sin modificar el código existente.

Variantes

VarianteDescripciónCaso de Uso
Bridge ClásicoDos jerarquías paralelasFormas y renderizadores, dispositivos y drivers
Bridge de DriversAbstracción sobre APIs de hardware/OSFrameworks UI multiplataforma
Bridge RemotoAbstracción local sobre implementación remotaStubs RPC y proxies

Lo que funciona

  • Identifica dimensiones independientes antes de aplicar el patrón — no todo problema de múltiples jerarquías necesita un bridge
  • Mantén la interfaz de implementación minimalista — solo expón lo que la abstracción necesita
  • Prefiere composición sobre herencia — el bridge es fundamentalmente sobre composición
  • Usa inyección de dependencias para conectar implementaciones en abstracciones
  • Documenta qué clase juega qué rol (abstracción vs. implementación) para los mantenedores

Errores Comunes

  • Aplicar el bridge cuando un simple strategy o adapter sería suficiente
  • Hacer la interfaz de implementación demasiado amplia, acoplandola innecesariamente a la abstracción
  • Permitir que la abstracción filtre detalles de implementación a los clientes
  • Crear jerarquías profundas en ambos lados, reintroduciendo la complejidad que el bridge intentaba resolver

Preguntas Frecuentes

P: ¿Cuál es la diferencia entre Bridge y Adapter? R: Adapter hace que interfaces incompatibles trabajen juntas. Bridge separa una abstracción de su implementación para que ambas puedan evolucionar independientemente. La intención y estructura difieren.

P: ¿Cuándo debería usar Bridge en lugar de Strategy? R: Strategy varía un solo algoritmo. Bridge separa dos jerarquías de clases completas. Usa Bridge cuando tengas dos dimensiones independientes de variación.

¿Es este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.

Temas Avanzados

Escenario: Bridge para Renderizado Multi-plataforma

// Bridge pattern: separar abstraccion de implementacion
interface Renderer {
  renderCircle(x: number, y: number, r: number): string;
  renderRect(x: number, y: number, w: number, h: number): string;
  renderText(x: number, y: number, text: string): 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}" />`; }
  renderText(x, y, text) { return `<text x="${x}" y="${y}">${text}</text>`; }
}

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});`; }
  renderText(x, y, text) { return `ctx.fillText("${text}", ${x}, ${y});`; }
}

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); }
}

class Rectangle extends Shape {
  constructor(renderer: Renderer, private x: number, private y: number, private w: number, private h: number) { super(renderer); }
  draw() { return this.renderer.renderRect(this.x, this.y, this.w, this.h); }
}

// Uso: mismas shapes, diferentes renderers
const svgCircle = new Circle(new SVGRenderer(), 50, 50, 20);
const canvasCircle = new Circle(new CanvasRenderer(), 50, 50, 20);
console.log(svgCircle.draw());   // <circle cx="50" cy="50" r="20" />
console.log(canvasCircle.draw()); // ctx.arc(50, 50, 20, 0, Math.PI * 2); ctx.stroke();

Lecciones:

  • Bridge separa abstraccion (Shape) de implementacion (Renderer)
  • Anadir nuevo renderer no requiere cambiar shapes
  • Anadir nuevo shape no requiere cambiar renderers
  • Reduce explosion de clases: M shapes + N renderers vs M*N
  • Flexibilidad en runtime: cambiar renderer sin tocar shape logic

### Bridge vs Adapter: cual uso?

Bridge es estructural: disenado desde el inicio para separar abstraccion de implementacion. Adapter es estructural: hace que interfaces incompatibles funcionen juntas. Bridge es proactivo: disenas ambos lados. Adapter es reactivo: envuelves una clase existente. Usa Bridge cuando controlas ambos lados y quieres evolucion independiente. Usa Adapter cuando necesitas integrar una API de terceros con una interfaz incompatible.


End of document. Review and update quarterly.