Este artículo explora el patrón de diseño Puente (Bridge Pattern), una técnica estructural para desacoplar una abstracción de su implementación, permitiendo que ambas evolucionen independientemente.
Análisis del Patrón Puente
El patrón Puente, también conocido como patrón "Puente" o "Bridge", es uno de los patrones de diseño más complejos de asimilar. Se define fundamentalmente por el principio de "desacoplar la abstracción de su implementación para que puedan variar independientemente". Una forma alternativa de entenderlo es que, cuando una clase presenta dos o más dimensiones de variabilidad independientes, el patrón Puente utiliza la composición para permitir que estas dimensiones se expandan de forma autónoma, evitando la explosión combinatoria que surgiría con la herencia.
Para ilustrar este concepto, consideremos el ejemplo del controlador JDBC. Cuando se utiliza JDBC para consultar una base de datos, la clase DriverManager y las implementaciones de java.sql.Driver (como com.mysql.jdbc.Driver) actúan como el puente.
// Carga y registra el driver específico de MySQL
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs = stmt.executeQuery(query);
while (rs.next()) {
// Procesar resultados
rs.getString(1);
rs.getInt(2);
}
Si deseamos cambiar de MySQL a Oracle, solo necesitamos modificar la línea que carga el driver. Una implementación más flexible podría leer el nombre del driver desde un archivo de configuración, eliminando la necesidad de alterar el código fuente para cambiar la base de datos subyacente.
Este desacoplamiento se logra porque, aunque DriverManager maneja la lógica general de conexión, delega las operaciones específicas a la implementación concreta del driver (com.mysql.jdbc.Driver en este caso). Todos los drivers implementan la interfaz java.sql.Driver, permitiendo esta conmutación sin afectar el código de la aplicación que utiliza JDBC.
En el contexto del patrón Puente:
- La abstracción es la interfaz JDBC en sí misma: un conjunto de funcionalidades independientes de la base de datos específica.
- La implementación son los drivers específicos de cada base de datos (
com.mysql.jdbc.Driver, etc.): el código que interactúa directamente con el motor de base de datos.
JDBC y el driver se desarrollan independientemente y se ensamblan mediante composición. Toda la lógica de JDBC se delega al driver correspondiente.
Ejemplo de Aplicación: Sistema de Notificación
Imaginemos un sistema de alerta para APIs que debe enviar notificaciones a través de diferentes canales (correo, SMS, WeChat) con distintos niveles de urgencia (SERVER, URGENCY, NORMAL, TRIVIAL).
Una implementación inicial podría verse así:
public enum NotificationEmergencyLevel {
SERVER, URGENCY, NORMAL, TRIVIAL
}
public class Notification {
// ... propiedades para direcciones de correo, teléfonos, IDs de WeChat ...
public void notify(NotificationEmergencyLevel level, String message) {
if (level.equals(NotificationEmergencyLevel.SERVER)) {
// Lógica para enviar por teléfono (voz)
} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
// Lógica para enviar por WeChat
} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
// Lógica para enviar por correo
} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
// Lógica para enviar por correo
}
}
}
Este enfoque tiene el inconveniente de un gran bloque if-else dentro de la clase Notification. Para mejorar la mantenibiliadd y extensibilidad, podemos aplicar el patrón Puente.
Separaremos la lógica de envío de mensajes en clases implementadoras independientes y utilizaremos una clase abstracta Notification que contendrá una referencia a estas implementaciones.
// Interfaz para los diferentes métodos de envío
public interface MessageSender {
void send(String message);
}
// Implementación para enviar por teléfono
public class TelephoneSender implements MessageSender {
private List<String> phoneNumbers;
public TelephoneSender(List<String> phoneNumbers) {
this.phoneNumbers = phoneNumbers;
}
@Override
public void send(String message) {
// Lógica de envío telefónico
System.out.println("Enviando por teléfono a " + phoneNumbers + ": " + message);
}
}
// Implementación para enviar por correo electrónico
public class EmailSender implements MessageSender {
private List<String> emailAddresses;
public EmailSender(List<String> emailAddresses) {
this.emailAddresses = emailAddresses;
}
@Override
public void send(String message) {
// Lógica de envío por email
System.out.println("Enviando por email a " + emailAddresses + ": " + message);
}
}
// Implementación para enviar por WeChat
public class WeChatSender implements MessageSender {
private List<String> weChatIDs;
public WeChatSender(List<String> weChatIDs) {
this.weChatIDs = weChatIDs;
}
@Override
public void send(String message) {
// Lógica de envío por WeChat
System.out.println("Enviando por WeChat a " + weChatIDs + ": " + message);
}
}
// Abstracción base para las notificaciones
public abstract class BaseNotification {
protected MessageSender sender;
public BaseNotification(MessageSender sender) {
this.sender = sender;
}
public abstract void notifyUser(String message);
}
// Notificación de nivel SERIOUS
public class SeriousNotification extends BaseNotification {
public SeriousNotification(MessageSender sender) {
super(sender);
}
@Override
public void notifyUser(String message) {
// Lógica específica si es necesario, luego delega al sender
sender.send("¡ALERTA SERIA!: " + message);
}
}
// Notificación de nivel URGENT
public class UrgentNotification extends BaseNotification {
public UrgentNotification(MessageSender sender) {
super(sender);
}
@Override
public void notifyUser(String message) {
sender.send("Notificación Urgente: " + message);
}
}
// Notificación de nivel NORMAL
public class NormalNotification extends BaseNotification {
public NormalNotification(MessageSender sender) {
super(sender);
}
@Override
public void notifyUser(String message) {
sender.send("Notificación Normal: " + message);
}
}
// Ejemplo de uso:
public class NotificationSystem {
public static void main(String[] args) {
// Crear diferentes remitentes
MessageSender phoneSender = new TelephoneSender(List.of("123-456-7890"));
MessageSender emailSender = new EmailSender(List.of("admin@example.com"));
MessageSender weChatSender = new WeChatSender(List.of("user123"));
// Crear diferentes tipos de notificaciones con sus respectivos remitentes
BaseNotification seriousAlert = new SeriousNotification(phoneSender);
BaseNotification urgentMessage = new UrgentNotification(weChatSender);
BaseNotification normalInfo = new NormalNotification(emailSender);
// Enviar notificaciones
seriousAlert.notifyUser("El servidor principal está caído.");
urgentMessage.notifyUser("Se detectó un pico de tráfico inusual.");
normalInfo.notifyUser("Actualización de mantenimiento completada.");
}
}
En este rediseño:
MessageSendery sus implementaciones (TelephoneSender,EmailSender,WeChatSender) representan las implementaciones.BaseNotificationy sus subclases (SeriousNotification,UrgentNotification,NormalNotification) representan las abstracciones.
La composición de un objeto BaseNotification con un objeto MessageSender crea el puente. Esto permite que los tipos de notificación y los canales de envío se combinen y modifiquen de forma independiente.
Resumen
El patrón Puente se enfoca en desacoplar la interfaz de un objeto de su implementación. Dos interpretaciones clave son:
- Enfoque GoF: "Desacoplar la abstracción de su implementación para que puedan variar independientemente". La "abstracción" se refiere a una biblioteca o framework conceptual, mientras que la "implementación" es una biblioteca o framework concreto que realiza la lógica real.
- Enfoque Alternativo: Utilizar la composición para manejar múltiples dimensiones de variabilidad independientes en una clase, evitando jerarquías de herencia complejas.
Ambas interpretaciones promueven la flexibilidad y la extensibilidad al permitir que las partes abstractas e implementadoras evolucionen por separado, conectadas a través de la composición.