El Bridge es un patrón que separa una abstracción de su implementación para que ambas evolucionen de forma independiente. La definición suena académica, pero el problema que resuelve es bastante concreto.
¿Qué significa esto realmente?
Significa que a veces la herencia nos juega una mala pasada. A veces, intentamos clasificar objetos por dos criterios diferentes a la vez (por ejemplo, forma y color, o sistema operativo y tipo de ventana), y acabamos creando una explosión combinatoria de clases.
El Bridge viene a romper esa herencia rígida y sustituirla por composición, creando dos jerarquías separadas unidas por un “puente”.
El Problema: La multiplicación de clases
Imagina que estamos desarrollando un motor gráfico multiplataforma.
Tenemos dos conceptos:
- Formas Geométricas: Círculo, Cuadrado.
- APIs de Renderizado: DirectX (Windows), OpenGL (Linux/Mac).
Si usamos herencia tradicional, empezaríamos creando una clase base Forma. Luego heredaríamos Circulo y Cuadrado.
Pero ahora tenemos que soportar las APIs. Así que acabamos con:
CirculoDirectXCirculoOpenGLCuadradoDirectXCuadradoOpenGL
Tenemos 4 clases.
Si mañana añadimos una nueva forma (Triangulo), necesitamos crear 2 clases más (una para cada API).
Y si añadimos una nueva API (Vulkan), necesitamos crear 3 clases más (una para cada forma).
El número de clases crece multiplicando: Formas x APIs. Esto es insostenible.
La Solución: Divide y Vencerás
El patrón Bridge propone dejar de intentar hacerlo todo en una sola jerarquía. En su lugar, creamos dos jerarquías separadas:
- Abstracción (El “Qué”): La jerarquía de las Formas (
Circulo,Cuadrado). - Implementación (El “Cómo”): La jerarquía del Renderizado (
DirectX,OpenGL).
Y la clase Forma tendrá una referencia (un puente) a un objeto de tipo Renderizador.
De esta forma, si añadimos una forma nueva, no tocamos los renderizadores. Y si añadimos un renderizador nuevo, no tocamos las formas. El crecimiento pasa de ser multiplicativo () a ser aditivo ().
Implementación en C#
Vamos a implementar nuestro motor gráfico simplificado.
La Interfaz de Implementación (El “Cómo”)
Primero definimos las operaciones básicas que cualquier motor gráfico debe saber hacer.
// Implementor
public interface IRenderizador
{
void RenderizarCirculo(float radio);
void RenderizarCuadrado(float lado);
}
// Concrete Implementor A
public class DirectXRenderer : IRenderizador
{
public void RenderizarCirculo(float radio)
{
Console.WriteLine($"[DirectX] Dibujando Círculo de radio {radio} usando shaders de Windows.");
}
public void RenderizarCuadrado(float lado)
{
Console.WriteLine($"[DirectX] Dibujando Cuadrado de lado {lado} usando shaders de Windows.");
}
}
// Concrete Implementor B
public class OpenGLRenderer : IRenderizador
{
public void RenderizarCirculo(float radio)
{
Console.WriteLine($"[OpenGL] Dibujando Círculo de radio {radio} en un buffer gráfico.");
}
public void RenderizarCuadrado(float lado)
{
Console.WriteLine($"[OpenGL] Dibujando Cuadrado de lado {lado} en un buffer gráfico.");
}
}La Abstracción (El “Qué”)
Ahora definimos las formas. Fíjate que la clase Forma recibe el IRenderizador en el constructor. Este es el puente.
// Abstraction
public abstract class Forma
{
protected IRenderizador _renderizador;
protected Forma(IRenderizador renderizador)
{
_renderizador = renderizador;
}
public abstract void Dibujar();
public abstract void Escalar(float porcentaje);
}Las Abstracciones Refinadas
Las formas concretas usan el puente para dibujarse, pero añaden su propia lógica de alto nivel.
// Refined Abstraction
public class Circulo : Forma
{
private float _radio;
public Circulo(IRenderizador renderizador, float radio) : base(renderizador)
{
_radio = radio;
}
public override void Dibujar()
{
// Delegamos el trabajo sucio al implementador
_renderizador.RenderizarCirculo(_radio);
}
public override void Escalar(float porcentaje)
{
_radio *= porcentaje;
}
}
public class Cuadrado : Forma
{
private float _lado;
public Cuadrado(IRenderizador renderizador, float lado) : base(renderizador)
{
_lado = lado;
}
public override void Dibujar()
{
_renderizador.RenderizarCuadrado(_lado);
}
public override void Escalar(float porcentaje)
{
_lado *= porcentaje;
}
}El Cliente (Mezclando y Emparejando)
Ahora podemos combinar cualquier forma con cualquier motor de renderizado en tiempo de ejecución.
class Program
{
static void Main(string[] args)
{
IRenderizador windows = new DirectXRenderer();
IRenderizador linux = new OpenGLRenderer();
// Círculo en Windows
Forma circuloWin = new Circulo(windows, 5.0f);
circuloWin.Dibujar();
// El MISMO tipo de objeto Círculo, pero en Linux
Forma circuloLinux = new Circulo(linux, 5.0f);
circuloLinux.Dibujar();
// Cuadrado en Windows
Forma cuadradoWin = new Cuadrado(windows, 10.0f);
cuadradoWin.Dibujar();
}
}Salida:
[DirectX] Dibujando Círculo de radio 5 usando shaders de Windows.
[OpenGL] Dibujando Círculo de radio 5 en un buffer gráfico.
[DirectX] Dibujando Cuadrado de lado 10 usando shaders de Windows.Fíjate en la potencia de esto: La clase Circulo no tiene ni idea de si está corriendo en DirectX o OpenGL. Solo sabe que tiene un _renderizador que sabe dibujar.
Bridge vs Adapter vs Strategy
Es fácil confundirse, así que aclaremos:
- Adapter: Hace que cosas que ya existen y son incompatibles, funcionen juntas. Se usa a posteriori.
- Bridge: Se diseña por adelantado. Sirve para estructurar el sistema y evitar la herencia masiva cuando tenemos dimensiones ortogonales (independientes) de variación.
- Strategy: Se parece mucho estructuralmente (un objeto delegando en una interfaz). Pero el Strategy se usa para cambiar un algoritmo o comportamiento específico (el “cómo se hace algo”), mientras que el Bridge es una estructura arquitectónica para separar una jerarquía completa.
¿Cuándo usar Bridge?
- Independencia: Cuando quieres que la implementación pueda cambiar en tiempo de ejecución (como cambiar temas visuales claro/oscuro en una UI).
- Explosión de clases: cuando creas subclases para cubrir todas las combinaciones posibles (
CocheRojo,CocheAzul,MotoRoja,MotoAzul…). Puede ser más flexible separarVehiculoyColor. - Desarrollo en paralelo: Un equipo puede trabajar en las Formas (lógica de negocio) y otro en los Renderizadores (drivers de bajo nivel) sin pisarse, ya que solo dependen de las interfaces.
El patrón Bridge es un salvavidas para la arquitectura a largo plazo. Nos permite mantener nuestras clases de alto nivel limpias y agnósticas de los detalles técnicos de implementación.