StackPractices
intermediate Por Mathias Paulenko

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.

Temas: design

Overview

classDiagram diagram: class Shape {

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

VarianteDescripciónCaso de uso
Bridge clásicoDos jerarquías paralelasFormas y renderizadores, dispositivos y drivers
Driver BridgeAbstracción sobre APIs de hardware o SOFrameworks de UI cross-platform
Remote BridgeAbstracción local sobre implementación remotaStubs 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

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.