Bridge
Desacopla uma abstração da sua implementação, para que as duas possam variar de forma independente.
Intenção
Dividir uma classe que tem duas dimensões ortogonais de variação em hierarquias separadas -- uma abstração e uma implementação -- conectadas por uma ponte (composição), para que qualquer um dos lados possa evoluir sem afetar o outro.
Problema
Você tem um conceito que pode variar em dois eixos independentes. Por exemplo, formas e backends de renderização, ou notificações e canais de entrega. Usar apenas herança leva a uma explosão combinatória de subclasses (CircleSvgRenderer, CircleCanvasRenderer, SquareSvgRenderer, ...) e torna adicionar uma nova variante em qualquer um dos eixos desproporcionalmente caro.
Solução
Extraia uma das dimensões para uma hierarquia de interface separada (a implementação). A hierarquia original (a abstração) mantém uma referência a um objeto de implementação e delega o trabalho específico de plataforma a ele. Ambas as hierarquias podem ser estendidas de forma independente, e novas combinações são formadas por composição em vez de subclassificação.
Participantes
- Abstraction -- define a interface de controle de alto nível; mantém uma referência a um Implementor
- RefinedAbstraction -- estende a Abstraction com comportamento mais rico
- Implementor -- declara a interface para operações específicas de implementação
- ConcreteImplementor -- fornece uma implementação concreta da interface Implementor
Vantagens
- Evita a explosão combinatória de subclasses vinda de duas dimensões ortogonais
- Abstração e implementação podem ser estendidas de forma independente
- Detalhes de implementação podem ser trocados ou substituídos em tempo de execução
- Segue o Princípio Aberto/Fechado -- novas abstrações e implementações não exigem modificar código existente
Desvantagens
- Aumenta a complexidade ao adicionar camadas extras de indireção
- Exige uma análise cuidadosa antecipada para identificar as dimensões corretas de variação
- Excessivo se houver apenas uma implementação ou se a abstração nunca mudar
Analogia do mundo real
Um controle remoto universal de TV (abstração) funciona com qualquer marca de televisão (implementação). O controle define botões como ligar, volume, e canal, enquanto cada marca de TV implementa como esses comandos são executados internamente. Você pode comprar um controle novo ou uma TV nova de forma independente -- eles se comunicam através de um protocolo infravermelho padrão (a ponte).
Casos de uso
- Formas que podem ser renderizadas com diferentes backends gráficos (SVG, Canvas, OpenGL)
- Notificações que podem ser entregues por diferentes canais (e-mail, SMS, push)
- Controles remotos de dispositivos que abstraem múltiplos tipos de aparelho (TV, rádio, smart speaker)
- Camadas de persistência onde objetos de negócio se conectam a diferentes backends de armazenamento (SQL, NoSQL, arquivo)
- Frameworks de UI multiplataforma onde widgets se conectam à renderização nativa da plataforma
Exemplos de código
interface Renderer {
void renderCircle(double x, double y, double radius);
void renderRect(double x, double y, double w, double h);
}
final class SvgRenderer implements Renderer {
@Override
public void renderCircle(double x, double y, double radius) {
System.out.printf("<circle cx="%.0f" cy="%.0f" r="%.0f" />%n", x, y, radius);
}
@Override
public void renderRect(double x, double y, double w, double h) {
System.out.printf("<rect x="%.0f" y="%.0f" width="%.0f" height="%.0f" />%n", x, y, w, h);
}
}
final class CanvasRenderer implements Renderer {
@Override
public void renderCircle(double x, double y, double radius) {
System.out.printf("ctx.arc(%.0f, %.0f, %.0f, 0, 2*PI); ctx.stroke();%n", x, y, radius);
}
@Override
public void renderRect(double x, double y, double w, double h) {
System.out.printf("ctx.strokeRect(%.0f, %.0f, %.0f, %.0f);%n", x, y, w, h);
}
}
abstract class Shape {
protected final Renderer renderer;
protected Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
final class Circle extends Shape {
private final double x, y, radius;
Circle(Renderer renderer, double x, double y, double radius) {
super(renderer);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
renderer.renderCircle(x, y, radius);
}
}
final class Rectangle extends Shape {
private final double x, y, w, h;
Rectangle(Renderer renderer, double x, double y, double w, double h) {
super(renderer);
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
@Override
public void draw() {
renderer.renderRect(x, y, w, h);
}
}
public class BridgeDemo {
public static void main(String[] args) {
// Same shapes, two independent rendering backends
Shape svgCircle = new Circle(new SvgRenderer(), 10, 10, 5);
Shape canvasCircle = new Circle(new CanvasRenderer(), 10, 10, 5);
svgCircle.draw();
canvasCircle.draw();
}
}Separa Shape (abstração) de Renderer (implementação) para que novas formas e novos backends de renderização possam ser adicionados de forma independente.
Padrões relacionados
Adapter
Converte a interface de uma classe em outra interface que os clientes esperam, permitindo que classes trabalhem juntas mesmo com interfaces incompatíveis.