Bridge Pattern - Separating Abstraction from Implementation

Learning Objectives: By the end of this lesson, you will be able to:

  • Understand the Bridge pattern and how it decouples abstraction from implementation
  • Implement bridge structures that allow independent evolution of abstractions and implementations
  • Apply Bridge pattern to cross-platform systems, device drivers, and plugin architectures
  • Design flexible hierarchies that avoid the “inheritance explosion” problem
  • Handle complex scenarios with multiple abstraction and implementation hierarchies

1. Welcome and Introduction (5 minutes)

Icebreaker Activity

Think about using a universal remote control that can operate different brands of TVs, sound systems, and streaming devices. The remote provides the same interface (power, volume, channel buttons) regardless of whether you’re controlling a Samsung TV, Sony sound system, or Apple TV. The remote’s interface is separate from how each device actually implements these operations. How would you design a software system with similar flexibility?

Lesson Overview

  • Duration: 120 minutes
  • Prerequisites: Object-oriented programming, inheritance, composition, interface design
  • Materials Needed: Development environment with Java, Python, or JavaScript support

2. Learning Objectives and Session Outline (5 minutes)

What We’ll Cover Today:

  1. Bridge Structure - Understanding abstraction, implementor, and their relationship
  2. Decoupling Benefits - How Bridge prevents inheritance explosion and enables independent evolution
  3. Implementation Strategies - Building bridges with interfaces, dependency injection, and factories
  4. Real-world Applications - Cross-platform GUIs, database drivers, notification systems
  5. Design Considerations - When to use Bridge vs other patterns, performance implications
  6. Advanced Techniques - Multiple bridges, bridge chains, and runtime implementation switching

Success Criteria:

By the end of this session, you should be able to design and implement bridge patterns that provide flexibility and maintainability in complex systems.

3. Foundation Knowledge (15-20 minutes)

Core Concepts

What is the Bridge Pattern?

The Bridge pattern decouples an abstraction from its implementation so that both can vary independently. It uses composition instead of inheritance to connect abstraction and implementation hierarchies.

Real-World Analogy: Think of a car’s steering wheel (abstraction) and the steering mechanism (implementation). Whether the car has power steering, manual steering, or electronic steering, you use the same steering wheel interface. The steering wheel provides the abstraction, while the actual steering mechanism can be implemented differently without changing how you interact with the wheel.

Why This Matters: Without Bridge pattern, you might end up with an explosion of subclasses when you need to support multiple abstractions across multiple implementations. Bridge pattern allows you to vary abstractions and implementations independently.

Bridge vs Inheritance Problem

Without Bridge: WindowsButton, MacButton, WindowsCheckbox, MacCheckbox, WindowsTextBox, MacTextBox… With Bridge: Button, Checkbox, TextBox (abstractions) + Windows, Mac (implementations)

Key Components

  • Abstraction: High-level interface used by clients
  • Refined Abstraction: Extended abstraction with additional features
  • Implementor: Interface for implementation classes
  • Concrete Implementor: Specific implementation of the implementor interface

Key Terminology

  • Abstraction Hierarchy: Family of classes that provide different high-level interfaces
  • Implementation Hierarchy: Family of classes that provide different low-level implementations
  • Bridge Interface: The connection point between abstraction and implementation
  • Composition over Inheritance: Using object composition instead of class inheritance
  • Platform Independence: Ability to work across different platforms or systems

4. Detailed Explanation and Examples (25-30 minutes)

Understanding Bridge Pattern Structure

The Bridge pattern is essential when:

  • You want to avoid permanent binding between abstraction and implementation
  • Both abstractions and implementations should be extensible through subclassing
  • Changes to implementation should not affect clients
  • You want to share implementation among multiple objects
  • You need to switch implementations at runtime

Practical Applications

Bridge pattern is commonly used for:

  • Cross-platform Development: GUI frameworks, graphics libraries, audio systems
  • Database Access: Different database vendors with common query interface
  • Device Drivers: Hardware abstraction layers, printer drivers, network adapters
  • Notification Systems: Multiple delivery channels (email, SMS, push) with various providers
  • Graphics Rendering: Different rendering engines (OpenGL, DirectX, software rendering)
  • Plugin Architectures: Extensible applications with swappable components

Common Misconceptions

  1. Misconception: Bridge is the same as Adapter pattern Reality: Bridge designs separation from the start; Adapter fixes existing incompatible interfaces

  2. Misconception: Bridge always involves exactly two hierarchies Reality: Can have multiple abstraction or implementation hierarchies

5. Live Coding Demonstration (20-25 minutes)

Example 1: Cross-Platform Notification System

// Implementation interface
interface NotificationSender {
    void sendNotification(String title, String message, String recipient);
    boolean isAvailable();
    void configure(Map<String, String> config);
    String getProviderName();
}

// Concrete Implementations
class EmailSender implements NotificationSender {
    private String smtpServer;
    private int port;
    private String username;
    private String password;
    
    @Override
    public void sendNotification(String title, String message, String recipient) {
        System.out.println("📧 EMAIL SENT");
        System.out.println("   To: " + recipient);
        System.out.println("   Subject: " + title);
        System.out.println("   Body: " + message);
        System.out.println("   Via: " + smtpServer + ":" + port);
    }
    
    @Override
    public boolean isAvailable() {
        // Check SMTP server connectivity
        return smtpServer != null && !smtpServer.isEmpty();
    }
    
    @Override
    public void configure(Map<String, String> config) {
        this.smtpServer = config.get("smtp.server");
        this.port = Integer.parseInt(config.getOrDefault("smtp.port", "587"));
        this.username = config.get("smtp.username");
        this.password = config.get("smtp.password");
        
        System.out.println("Email sender configured for: " + smtpServer);
    }
    
    @Override
    public String getProviderName() {
        return "Email Provider";
    }
}

class SMSSender implements NotificationSender {
    private String apiKey;
    private String serviceUrl;
    private String fromNumber;
    
    @Override
    public void sendNotification(String title, String message, String recipient) {
        System.out.println("📱 SMS SENT");
        System.out.println("   To: " + recipient);
        System.out.println("   From: " + fromNumber);
        System.out.println("   Message: " + title + " - " + message);
        System.out.println("   Via: " + serviceUrl);
    }
    
    @Override
    public boolean isAvailable() {
        return apiKey != null && serviceUrl != null;
    }
    
    @Override
    public void configure(Map<String, String> config) {
        this.apiKey = config.get("sms.api.key");
        this.serviceUrl = config.get("sms.service.url");
        this.fromNumber = config.get("sms.from.number");
        
        System.out.println("SMS sender configured for: " + serviceUrl);
    }
    
    @Override
    public String getProviderName() {
        return "SMS Provider";
    }
}

class PushNotificationSender implements NotificationSender {
    private String appId;
    private String apiKey;
    private String certificatePath;
    
    @Override
    public void sendNotification(String title, String message, String recipient) {
        System.out.println("🔔 PUSH NOTIFICATION SENT");
        System.out.println("   To Device: " + recipient);
        System.out.println("   Title: " + title);
        System.out.println("   Message: " + message);
        System.out.println("   App ID: " + appId);
    }
    
    @Override
    public boolean isAvailable() {
        return appId != null && apiKey != null;
    }
    
    @Override
    public void configure(Map<String, String> config) {
        this.appId = config.get("push.app.id");
        this.apiKey = config.get("push.api.key");
        this.certificatePath = config.get("push.certificate.path");
        
        System.out.println("Push notification configured for app: " + appId);
    }
    
    @Override
    public String getProviderName() {
        return "Push Notification Provider";
    }
}

class SlackSender implements NotificationSender {
    private String webhookUrl;
    private String channel;
    private String botName;
    
    @Override
    public void sendNotification(String title, String message, String recipient) {
        System.out.println("💬 SLACK MESSAGE SENT");
        System.out.println("   Channel: " + (recipient.startsWith("#") ? recipient : "#" + recipient));
        System.out.println("   Title: " + title);
        System.out.println("   Message: " + message);
        System.out.println("   Bot: " + botName);
    }
    
    @Override
    public boolean isAvailable() {
        return webhookUrl != null && !webhookUrl.isEmpty();
    }
    
    @Override
    public void configure(Map<String, String> config) {
        this.webhookUrl = config.get("slack.webhook.url");
        this.channel = config.get("slack.default.channel");
        this.botName = config.getOrDefault("slack.bot.name", "NotificationBot");
        
        System.out.println("Slack sender configured for: " + channel);
    }
    
    @Override
    public String getProviderName() {
        return "Slack Provider";
    }
}

// Abstraction
abstract class Notification {
    protected NotificationSender sender;
    protected String title;
    protected String message;
    protected Date timestamp;
    
    public Notification(NotificationSender sender, String title, String message) {
        this.sender = sender;
        this.title = title;
        this.message = message;
        this.timestamp = new Date();
    }
    
    public abstract void send(String recipient);
    
    public void setSender(NotificationSender sender) {
        this.sender = sender;
    }
    
    public boolean canSend() {
        return sender != null && sender.isAvailable();
    }
    
    public String getProviderName() {
        return sender != null ? sender.getProviderName() : "No Provider";
    }
    
    // Common functionality
    protected String formatMessage() {
        return message;
    }
    
    protected void logNotification(String recipient) {
        System.out.println("📝 LOG: Notification sent at " + timestamp + 
                          " to " + recipient + " via " + getProviderName());
    }
}

// Refined Abstractions
class AlertNotification extends Notification {
    private AlertLevel level;
    
    public enum AlertLevel { INFO, WARNING, ERROR, CRITICAL }
    
    public AlertNotification(NotificationSender sender, String title, String message, AlertLevel level) {
        super(sender, title, message);
        this.level = level;
    }
    
    @Override
    public void send(String recipient) {
        if (!canSend()) {
            System.out.println("❌ Cannot send alert: " + (sender == null ? "No sender" : "Sender unavailable"));
            return;
        }
        
        String formattedTitle = formatAlertTitle();
        String formattedMessage = formatMessage();
        
        sender.sendNotification(formattedTitle, formattedMessage, recipient);
        logNotification(recipient);
    }
    
    private String formatAlertTitle() {
        String emoji = getAlertEmoji();
        return emoji + " [" + level + "] " + title;
    }
    
    @Override
    protected String formatMessage() {
        return "Alert Level: " + level + "\n" +
               "Time: " + timestamp + "\n" +
               "Details: " + message + "\n" +
               "Please take appropriate action based on the alert level.";
    }
    
    private String getAlertEmoji() {
        switch (level) {
            case INFO: return "ℹ️";
            case WARNING: return "⚠️";
            case ERROR: return "❌";
            case CRITICAL: return "🚨";
            default: return "📢";
        }
    }
    
    public AlertLevel getLevel() {
        return level;
    }
}

class MarketingNotification extends Notification {
    private String campaign;
    private String callToAction;
    
    public MarketingNotification(NotificationSender sender, String title, String message, 
                               String campaign, String callToAction) {
        super(sender, title, message);
        this.campaign = campaign;
        this.callToAction = callToAction;
    }
    
    @Override
    public void send(String recipient) {
        if (!canSend()) {
            System.out.println("❌ Cannot send marketing notification: " + 
                             (sender == null ? "No sender" : "Sender unavailable"));
            return;
        }
        
        String formattedTitle = "🎯 " + title;
        String formattedMessage = formatMessage();
        
        sender.sendNotification(formattedTitle, formattedMessage, recipient);
        logNotification(recipient);
        trackCampaign(recipient);
    }
    
    @Override
    protected String formatMessage() {
        return message + "\n\n" +
               "Campaign: " + campaign + "\n" +
               "🔗 " + callToAction + "\n\n" +
               "Thanks for being a valued customer!";
    }
    
    private void trackCampaign(String recipient) {
        System.out.println("📊 ANALYTICS: Marketing notification tracked for campaign '" + 
                          campaign + "' sent to " + recipient);
    }
    
    public String getCampaign() {
        return campaign;
    }
    
    public String getCallToAction() {
        return callToAction;
    }
}

class TransactionalNotification extends Notification {
    private String transactionId;
    private String accountNumber;
    private double amount;
    
    public TransactionalNotification(NotificationSender sender, String title, String message,
                                   String transactionId, String accountNumber, double amount) {
        super(sender, title, message);
        this.transactionId = transactionId;
        this.accountNumber = accountNumber;
        this.amount = amount;
    }
    
    @Override
    public void send(String recipient) {
        if (!canSend()) {
            System.out.println("❌ Cannot send transaction notification: " + 
                             (sender == null ? "No sender" : "Sender unavailable"));
            return;
        }
        
        String formattedTitle = "💳 " + title;
        String formattedMessage = formatMessage();
        
        sender.sendNotification(formattedTitle, formattedMessage, recipient);
        logNotification(recipient);
        recordTransaction(recipient);
    }
    
    @Override
    protected String formatMessage() {
        return message + "\n\n" +
               "Transaction ID: " + transactionId + "\n" +
               "Account: " + maskAccount(accountNumber) + "\n" +
               "Amount: $" + String.format("%.2f", amount) + "\n" +
               "Date: " + timestamp + "\n\n" +
               "If you did not authorize this transaction, please contact us immediately.";
    }
    
    private String maskAccount(String account) {
        if (account.length() <= 4) return account;
        return "*".repeat(account.length() - 4) + account.substring(account.length() - 4);
    }
    
    private void recordTransaction(String recipient) {
        System.out.println("🏦 AUDIT: Transaction notification recorded - ID: " + 
                          transactionId + ", Recipient: " + recipient);
    }
    
    public String getTransactionId() {
        return transactionId;
    }
    
    public double getAmount() {
        return amount;
    }
}

Example 2: Graphics Rendering Bridge

// Implementation interface for graphics rendering
interface GraphicsRenderer {
    void initialize(int width, int height);
    void clear(Color backgroundColor);
    void drawLine(int x1, int y1, int x2, int y2, Color color, int thickness);
    void drawRectangle(int x, int y, int width, int height, Color color, boolean filled);
    void drawCircle(int centerX, int centerY, int radius, Color color, boolean filled);
    void drawText(String text, int x, int y, Font font, Color color);
    void present(); // Display the rendered frame
    void shutdown();
    String getRendererName();
}

// Concrete implementations
class OpenGLRenderer implements GraphicsRenderer {
    private int width, height;
    private boolean initialized = false;
    
    @Override
    public void initialize(int width, int height) {
        this.width = width;
        this.height = height;
        this.initialized = true;
        System.out.println("🎮 OpenGL Renderer initialized: " + width + "x" + height);
    }
    
    @Override
    public void clear(Color backgroundColor) {
        System.out.println("🎮 OpenGL: Clearing with color " + backgroundColor);
    }
    
    @Override
    public void drawLine(int x1, int y1, int x2, int y2, Color color, int thickness) {
        System.out.println("🎮 OpenGL: Drawing line from (" + x1 + "," + y1 + ") to (" + 
                          x2 + "," + y2 + ") with thickness " + thickness);
    }
    
    @Override
    public void drawRectangle(int x, int y, int width, int height, Color color, boolean filled) {
        String type = filled ? "filled" : "outline";
        System.out.println("🎮 OpenGL: Drawing " + type + " rectangle at (" + x + "," + y + 
                          ") size " + width + "x" + height);
    }
    
    @Override
    public void drawCircle(int centerX, int centerY, int radius, Color color, boolean filled) {
        String type = filled ? "filled" : "outline";
        System.out.println("🎮 OpenGL: Drawing " + type + " circle at (" + centerX + "," + 
                          centerY + ") radius " + radius);
    }
    
    @Override
    public void drawText(String text, int x, int y, Font font, Color color) {
        System.out.println("🎮 OpenGL: Drawing text '" + text + "' at (" + x + "," + y + ")");
    }
    
    @Override
    public void present() {
        System.out.println("🎮 OpenGL: Swapping buffers and presenting frame");
    }
    
    @Override
    public void shutdown() {
        initialized = false;
        System.out.println("🎮 OpenGL: Renderer shutdown");
    }
    
    @Override
    public String getRendererName() {
        return "OpenGL Hardware Renderer";
    }
}

class DirectXRenderer implements GraphicsRenderer {
    private int width, height;
    private boolean initialized = false;
    
    @Override
    public void initialize(int width, int height) {
        this.width = width;
        this.height = height;
        this.initialized = true;
        System.out.println("🖥️ DirectX Renderer initialized: " + width + "x" + height);
    }
    
    @Override
    public void clear(Color backgroundColor) {
        System.out.println("🖥️ DirectX: Clearing render target with color " + backgroundColor);
    }
    
    @Override
    public void drawLine(int x1, int y1, int x2, int y2, Color color, int thickness) {
        System.out.println("🖥️ DirectX: Rendering line primitive from (" + x1 + "," + y1 + 
                          ") to (" + x2 + "," + y2 + ") thickness " + thickness);
    }
    
    @Override
    public void drawRectangle(int x, int y, int width, int height, Color color, boolean filled) {
        String primitive = filled ? "quad" : "line loop";
        System.out.println("🖥️ DirectX: Rendering " + primitive + " at (" + x + "," + y + 
                          ") size " + width + "x" + height);
    }
    
    @Override
    public void drawCircle(int centerX, int centerY, int radius, Color color, boolean filled) {
        String primitive = filled ? "triangle fan" : "line strip";
        System.out.println("🖥️ DirectX: Rendering circle as " + primitive + " at (" + 
                          centerX + "," + centerY + ") radius " + radius);
    }
    
    @Override
    public void drawText(String text, int x, int y, Font font, Color color) {
        System.out.println("🖥️ DirectX: Rendering text '" + text + "' using font textures at (" + 
                          x + "," + y + ")");
    }
    
    @Override
    public void present() {
        System.out.println("🖥️ DirectX: Presenting back buffer to screen");
    }
    
    @Override
    public void shutdown() {
        initialized = false;
        System.out.println("🖥️ DirectX: Releasing D3D resources and shutting down");
    }
    
    @Override
    public String getRendererName() {
        return "DirectX Hardware Renderer";
    }
}

class SoftwareRenderer implements GraphicsRenderer {
    private int width, height;
    private boolean initialized = false;
    private int[][] frameBuffer;
    
    @Override
    public void initialize(int width, int height) {
        this.width = width;
        this.height = height;
        this.frameBuffer = new int[height][width];
        this.initialized = true;
        System.out.println("💻 Software Renderer initialized: " + width + "x" + height + 
                          " (CPU-based rendering)");
    }
    
    @Override
    public void clear(Color backgroundColor) {
        System.out.println("💻 Software: Clearing frame buffer with color " + backgroundColor);
    }
    
    @Override
    public void drawLine(int x1, int y1, int x2, int y2, Color color, int thickness) {
        System.out.println("💻 Software: CPU line drawing from (" + x1 + "," + y1 + ") to (" + 
                          x2 + "," + y2 + ") using Bresenham algorithm");
    }
    
    @Override
    public void drawRectangle(int x, int y, int width, int height, Color color, boolean filled) {
        String method = filled ? "fill algorithm" : "border drawing";
        System.out.println("💻 Software: CPU rectangle rendering using " + method + 
                          " at (" + x + "," + y + ") size " + width + "x" + height);
    }
    
    @Override
    public void drawCircle(int centerX, int centerY, int radius, Color color, boolean filled) {
        String algorithm = filled ? "flood fill" : "midpoint circle";
        System.out.println("💻 Software: CPU circle rendering using " + algorithm + 
                          " at (" + centerX + "," + centerY + ") radius " + radius);
    }
    
    @Override
    public void drawText(String text, int x, int y, Font font, Color color) {
        System.out.println("💻 Software: CPU text rasterization '" + text + "' at (" + x + "," + y + 
                          ") using bitmap fonts");
    }
    
    @Override
    public void present() {
        System.out.println("💻 Software: Copying frame buffer to display surface");
    }
    
    @Override
    public void shutdown() {
        frameBuffer = null;
        initialized = false;
        System.out.println("💻 Software: Renderer shutdown, frame buffer released");
    }
    
    @Override
    public String getRendererName() {
        return "Software CPU Renderer";
    }
}

// Abstraction
abstract class Shape {
    protected GraphicsRenderer renderer;
    protected Color color;
    protected int x, y;
    
    public Shape(GraphicsRenderer renderer, int x, int y, Color color) {
        this.renderer = renderer;
        this.x = x;
        this.y = y;
        this.color = color;
    }
    
    public abstract void draw();
    
    public void setRenderer(GraphicsRenderer renderer) {
        this.renderer = renderer;
    }
    
    public void move(int newX, int newY) {
        this.x = newX;
        this.y = newY;
    }
    
    public void setColor(Color color) {
        this.color = color;
    }
}

// Refined Abstractions
class Rectangle extends Shape {
    private int width, height;
    private boolean filled;
    
    public Rectangle(GraphicsRenderer renderer, int x, int y, int width, int height, 
                    Color color, boolean filled) {
        super(renderer, x, y, color);
        this.width = width;
        this.height = height;
        this.filled = filled;
    }
    
    @Override
    public void draw() {
        renderer.drawRectangle(x, y, width, height, color, filled);
    }
    
    public void resize(int newWidth, int newHeight) {
        this.width = newWidth;
        this.height = newHeight;
    }
    
    public void setFilled(boolean filled) {
        this.filled = filled;
    }
}

class Circle extends Shape {
    private int radius;
    private boolean filled;
    
    public Circle(GraphicsRenderer renderer, int centerX, int centerY, int radius, 
                 Color color, boolean filled) {
        super(renderer, centerX, centerY, color);
        this.radius = radius;
        this.filled = filled;
    }
    
    @Override
    public void draw() {
        renderer.drawCircle(x, y, radius, color, filled);
    }
    
    public void setRadius(int radius) {
        this.radius = radius;
    }
    
    public void setFilled(boolean filled) {
        this.filled = filled;
    }
}

class Text extends Shape {
    private String content;
    private Font font;
    
    public Text(GraphicsRenderer renderer, int x, int y, String content, Font font, Color color) {
        super(renderer, x, y, color);
        this.content = content;
        this.font = font;
    }
    
    @Override
    public void draw() {
        renderer.drawText(content, x, y, font, color);
    }
    
    public void setText(String content) {
        this.content = content;
    }
    
    public void setFont(Font font) {
        this.font = font;
    }
}

// Scene management
class Scene {
    private List<Shape> shapes;
    private GraphicsRenderer renderer;
    private Color backgroundColor;
    
    public Scene(GraphicsRenderer renderer) {
        this.renderer = renderer;
        this.shapes = new ArrayList<>();
        this.backgroundColor = Color.BLACK;
    }
    
    public void addShape(Shape shape) {
        shapes.add(shape);
    }
    
    public void setRenderer(GraphicsRenderer newRenderer) {
        this.renderer = newRenderer;
        // Update all shapes to use new renderer
        for (Shape shape : shapes) {
            shape.setRenderer(newRenderer);
        }
    }
    
    public void render() {
        renderer.clear(backgroundColor);
        
        for (Shape shape : shapes) {
            shape.draw();
        }
        
        renderer.present();
    }
    
    public void setBackgroundColor(Color color) {
        this.backgroundColor = color;
    }
    
    public String getRendererInfo() {
        return renderer.getRendererName();
    }
}

Example 3: Usage Demonstrations

public class BridgePatternDemo {
    public static void demonstrateNotificationSystem() {
        System.out.println("=== Notification System Bridge Demo ===");
        
        // Configure different senders
        Map<String, String> emailConfig = new HashMap<>();
        emailConfig.put("smtp.server", "smtp.company.com");
        emailConfig.put("smtp.port", "587");
        emailConfig.put("smtp.username", "notifications@company.com");
        emailConfig.put("smtp.password", "secret");
        
        Map<String, String> smsConfig = new HashMap<>();
        smsConfig.put("sms.api.key", "abc123");
        smsConfig.put("sms.service.url", "https://api.sms-provider.com");
        smsConfig.put("sms.from.number", "+1234567890");
        
        Map<String, String> slackConfig = new HashMap<>();
        slackConfig.put("slack.webhook.url", "https://hooks.slack.com/services/...");
        slackConfig.put("slack.default.channel", "alerts");
        slackConfig.put("slack.bot.name", "AlertBot");
        
        // Create senders
        NotificationSender emailSender = new EmailSender();
        emailSender.configure(emailConfig);
        
        NotificationSender smsSender = new SMSSender();
        smsSender.configure(smsConfig);
        
        NotificationSender slackSender = new SlackSender();
        slackSender.configure(slackConfig);
        
        // Create different types of notifications with different senders
        AlertNotification criticalAlert = new AlertNotification(
            emailSender, 
            "System Down", 
            "Primary database server is not responding", 
            AlertNotification.AlertLevel.CRITICAL
        );
        
        MarketingNotification promotion = new MarketingNotification(
            smsSender,
            "Flash Sale",
            "Get 50% off on all items today only!",
            "flash-sale-2025",
            "Shop now at: www.example.com/sale"
        );
        
        TransactionalNotification payment = new TransactionalNotification(
            slackSender,
            "Payment Processed",
            "Your payment has been successfully processed",
            "TXN-98765",
            "1234567890123456",
            299.99
        );
        
        // Send notifications
        System.out.println("\n--- Sending Notifications ---");
        criticalAlert.send("admin@company.com");
        promotion.send("+1987654321");
        payment.send("finance");
        
        // Switch implementations at runtime
        System.out.println("\n--- Switching to Different Sender ---");
        criticalAlert.setSender(slackSender);
        criticalAlert.send("infrastructure-alerts");
    }
    
    public static void demonstrateGraphicsRendering() {
        System.out.println("\n=== Graphics Rendering Bridge Demo ===");
        
        // Create different renderers
        GraphicsRenderer openGL = new OpenGLRenderer();
        GraphicsRenderer directX = new DirectXRenderer();
        GraphicsRenderer software = new SoftwareRenderer();
        
        // Initialize renderers
        openGL.initialize(1920, 1080);
        directX.initialize(1920, 1080);
        software.initialize(800, 600);
        
        // Create scene with OpenGL renderer
        Scene scene = new Scene(openGL);
        
        // Add shapes to scene
        Rectangle rect = new Rectangle(openGL, 100, 100, 200, 150, Color.BLUE, true);
        Circle circle = new Circle(openGL, 400, 300, 75, Color.RED, false);
        Text title = new Text(openGL, 50, 50, "Bridge Pattern Demo", new Font("Arial", Font.BOLD, 24), Color.WHITE);
        
        scene.addShape(rect);
        scene.addShape(circle);
        scene.addShape(title);
        
        // Render with OpenGL
        System.out.println("\n--- Rendering with " + scene.getRendererInfo() + " ---");
        scene.render();
        
        // Switch to DirectX renderer at runtime
        System.out.println("\n--- Switching to DirectX Renderer ---");
        scene.setRenderer(directX);
        System.out.println("Now using: " + scene.getRendererInfo());
        scene.render();
        
        // Switch to Software renderer
        System.out.println("\n--- Switching to Software Renderer ---");
        scene.setRenderer(software);
        System.out.println("Now using: " + scene.getRendererInfo());
        scene.render();
        
        // Cleanup
        openGL.shutdown();
        directX.shutdown();
        software.shutdown();
    }
    
    public static void main(String[] args) {
        demonstrateNotificationSystem();
        demonstrateGraphicsRendering();
    }
}

6. Hands-On Practice (30-40 minutes)

Exercise 1: Basic Practice (10 minutes)

Objective: Implement a simple bridge for data persistence

Instructions:

  1. Create a DataStore interface with methods: save(), load(), delete(), exists()
  2. Implement concrete stores: FileDataStore, DatabaseDataStore, MemoryDataStore
  3. Create a Document abstraction that uses DataStore
  4. Test switching between different storage implementations

Starter Code:

interface DataStore {
    void save(String key, String data);
    String load(String key);
    boolean delete(String key);
    boolean exists(String key);
}

abstract class Document {
    protected DataStore store;
    protected String content;
    protected String filename;
    
    // TODO: Implement document operations using the store
}

Exercise 2: Intermediate Challenge (15 minutes)

Objective: Build a media player bridge with different audio backends

Requirements:

  • Create AudioBackend interface with play(), pause(), stop(), setVolume()
  • Implement backends: DirectSoundBackend, OpenALBackend, PulseAudioBackend
  • Create MediaPlayer abstraction with playlist functionality
  • Support format-specific optimizations in different backends
  • Add audio effects that work with any backend

Hints:

  • Consider how different backends handle audio formats
  • Think about platform-specific optimizations
  • Design for easy addition of new audio effects

Exercise 3: Creative Application (15 minutes)

Objective: Design a payment processing bridge

Challenge: Create a payment system that supports:

  • Different payment gateways (PayPal, Stripe, Square, Bank Transfer)
  • Various payment types (credit card, bank transfer, digital wallet)
  • Different currencies and conversion handling
  • Fraud detection that works with any gateway
  • Transaction logging and reconciliation

Extension Ideas:

  • Add retry mechanisms for failed payments
  • Implement payment routing based on amount or region
  • Support split payments across multiple gateways

7. Common Pitfalls and Troubleshooting (10 minutes)

Frequent Mistakes

  1. Problem: Putting too much logic in the abstraction layer Cause: Not understanding the separation between abstraction and implementation Solution: Keep abstractions focused on interface; move complex logic to implementations

  2. Problem: Creating unnecessary abstraction hierarchies Cause: Over-engineering when simple inheritance would suffice Solution: Use Bridge only when you need independent evolution of both sides

  3. Problem: Tight coupling between abstraction and implementation Cause: Abstraction knowing too much about specific implementations Solution: Design generic interfaces and use dependency injection

Debugging Tips

  • Use factory patterns to create and configure bridges properly
  • Add logging to track which implementations are being used
  • Test with multiple implementation combinations to ensure proper decoupling
  • Validate that changing implementations doesn’t break functionality

8. Best Practices and Professional Tips (5 minutes)

Industry Standards

  • Use dependency injection for providing implementations to abstractions
  • Design implementations to be stateless when possible for easier testing
  • Provide factory methods for creating commonly used bridge combinations
  • Document the bridge contract clearly for both abstraction and implementation sides

Design Principles

  • Keep interfaces focused - don’t mix abstraction concerns with implementation details
  • Plan for extension - design bridges to support future implementations
  • Favor composition over inheritance for connecting bridges
  • Minimize bridge interface - only include what’s truly needed

Performance Considerations

  • Consider call overhead - bridges add indirection
  • Cache implementations when creation is expensive
  • Profile different implementations to understand performance characteristics
  • Design for lazy initialization when implementations are resource-intensive

Next Steps in Your Learning Journey

  • Study Adapter pattern for fixing incompatible interfaces
  • Learn Strategy pattern for algorithm selection vs platform abstraction
  • Explore Abstract Factory for creating families of bridge implementations
  • Investigate Dependency Injection frameworks for advanced bridge management

9. Summary and Review (5 minutes)

Key Takeaways

  1. Bridge separates abstraction from implementation allowing independent evolution
  2. Prevents inheritance explosion by using composition instead of inheritance
  3. Enables runtime implementation switching for flexibility
  4. Perfect for cross-platform development and plugin architectures

Quick Review Questions

  1. How does Bridge pattern differ from Adapter pattern?
  2. When would you choose Bridge over simple inheritance?
  3. What are the trade-offs between flexibility and simplicity with Bridge pattern?

For Instructors

Teaching Tips

  • Start with inheritance explosion problem to motivate the need for Bridge
  • Use platform examples (Windows/Mac, OpenGL/DirectX) that students can relate to
  • Show runtime switching to demonstrate the power of the pattern
  • Compare with Adapter pattern to clarify the differences
  • Use UML diagrams to show the dual hierarchy structure

Common Student Questions

  • “When to use Bridge vs Adapter?” - Explain design-time vs runtime decisions
  • “Isn’t this just dependency injection?” - Discuss the structural vs behavioral aspects
  • “How many implementations is too many?” - Talk about complexity vs flexibility trade-offs

Assessment Ideas

  • Design challenge: Create bridge for specific domain (logging, caching, communication)
  • Refactoring exercise: Convert inheritance hierarchy to bridge pattern
  • Performance analysis: Compare bridge overhead vs direct implementation calls

Adaptation Notes

  • Beginner focus: Start with simple examples before complex multi-implementation scenarios
  • Advanced students: Discuss advanced topics like bridge chains, multiple bridges, performance optimization
  • Language-specific: Show language-specific bridge implementations (interfaces vs abstract classes)
  • Time management: Graphics rendering example can be simplified for shorter sessions