Bridge Pattern

The Bridge Pattern is a structural design pattern that aims to separate the abstraction (an interface or abstract class) from its implementation (the concrete classes that provide the functionality). This separation allows both the abstraction and the implementation to vary independently, making the design more flexible and adaptable to changes.

The Bridge Pattern is a structural design pattern that decouples an abstraction from its implementation, allowing the two to vary independently. It’s particularly useful when you want to separate an object’s interface from its implementation details or when you need to support multiple implementations for the same abstraction.

Important Components

  1. Abstraction: Defines the abstract interface and holds a reference to the Implementor.
  2. Refined Abstraction: Extends the Abstraction, providing additional or specific functionality.
  3. Implementor: Defines the interface for implementation classes (often an interface or abstract class).
  4. Concrete Implementor: Provides specific implementations of the Implementor interface.

How It Works

  • The Abstraction defines high-level operations and delegates implementation details to the Implementor.
  • The Implementor provides a low-level interface for the actual work.
  • The client interacts with the Abstraction, which uses the Implementor to perform tasks.
  • This separation allows you to change the implementation without modifying the abstraction (and vice versa).

Sample Implementation

Let’s model a scenario where a Shape (Abstraction) can be drawn using different rendering methods (Implementor), such as raster or vector graphics.

// Implementor Interface
interface Renderer {
    void renderShape(String shapeName);
}

// Concrete Implementors
class RasterRenderer implements Renderer {
    @Override
    public void renderShape(String shapeName) {
        System.out.println("Rendering " + shapeName + " using Raster graphics");
    }
}

class VectorRenderer implements Renderer {
    @Override
    public void renderShape(String shapeName) {
        System.out.println("Rendering " + shapeName + " using Vector graphics");
    }
}

// Abstraction
abstract class Shape {
    protected Renderer renderer;

    public Shape(Renderer renderer) {
        this.renderer = renderer;
    }

    abstract void draw();
}

// Refined Abstractions
class Circle extends Shape {
    public Circle(Renderer renderer) {
        super(renderer);
    }

    @Override
    public void draw() {
        renderer.renderShape("Circle");
    }
}

class Square extends Shape {
    public Square(Renderer renderer) {
        super(renderer);
    }

    @Override
    public void draw() {
        renderer.renderShape("Square");
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Create renderers (Implementors)
        Renderer raster = new RasterRenderer();
        Renderer vector = new VectorRenderer();

        // Create shapes with different renderers
        Shape circleRaster = new Circle(raster);
        Shape circleVector = new Circle(vector);
        Shape squareRaster = new Square(raster);

        // Draw shapes
        circleRaster.draw();
        circleVector.draw();
        squareRaster.draw();
    }
}

/*
Rendering Circle using Raster graphics
Rendering Circle using Vector graphics
Rendering Square using Raster graphics
*/Code language: PHP (php)

Use case and Implementation

Separating the abstraction of `Account` types from their specific implementations. (e.g., different storage methods or transaction processing strategies.)

//BridgePatternDemo.java
// Implementor interface
interface Storage {
    void storeAccount(String accountName, double balance);
    double retrieveAccountBalance(String accountName);
}

// Concrete Implementor: FileStorage
class FileStorage implements Storage {
    @Override
    public void storeAccount(String accountName, double balance) {
        // Simplified implementation to store account data in a file
        System.out.println("Storing account " + accountName + " with balance " + balance + " to file.");
        // Actual file storage logic would go here
    }

    @Override
    public double retrieveAccountBalance(String accountName) {
        // Simplified implementation to retrieve account balance from a file
        System.out.println("Retrieving balance of account " + accountName + " from file.");
        // Actual file retrieval logic would go here
        return 1000.0; // Placeholder return for demonstration
    }
}

// Concrete Implementor: DatabaseStorage
class DatabaseStorage implements Storage {
    @Override
    public void storeAccount(String accountName, double balance) {
        // Simplified implementation to store account data in a database
        System.out.println("Storing account " + accountName + " with balance " + balance + " to database.");
        // Actual database storage logic would go here
    }

    @Override
    public double retrieveAccountBalance(String accountName) {
        // Simplified implementation to retrieve account balance from a database
        System.out.println("Retrieving balance of account " + accountName + " from database.");
        // Actual database retrieval logic would go here
        return 5000.0; // Placeholder return for demonstration
    }
}

// Abstraction: Account
abstract class Account {
    protected Storage storage;
    protected String accountName;

    public Account(String accountName, Storage storage) {
        this.accountName = accountName;
        this.storage = storage;
    }

    public abstract void deposit(double amount);
    public abstract void withdraw(double amount);
    public abstract double getBalance();
}

// Refined Abstraction: SavingsAccount
class SavingsAccount extends Account {

    public SavingsAccount(String accountName, Storage storage) {
        super(accountName, storage);
    }

    @Override
    public void deposit(double amount) {
        // Business logic for depositing into savings account
        double currentBalance = storage.retrieveAccountBalance(accountName);
        double newBalance = currentBalance + amount;
        storage.storeAccount(accountName, newBalance);
        System.out.println(amount + " deposited into Savings Account " + accountName);
    }

    @Override
    public void withdraw(double amount) {
        // Business logic for withdrawing from savings account
        double currentBalance = storage.retrieveAccountBalance(accountName);
        if (currentBalance >= amount) {
            double newBalance = currentBalance - amount;
            storage.storeAccount(accountName, newBalance);
            System.out.println(amount + " withdrawn from Savings Account " + accountName);
        } else {
            System.out.println("Insufficient balance in Savings Account " + accountName);
        }
    }

    @Override
    public double getBalance() {
        // Business logic to get balance of savings account
        return storage.retrieveAccountBalance(accountName);
    }
}

// Refined Abstraction: CheckingAccount
class CheckingAccount extends Account {

    public CheckingAccount(String accountName, Storage storage) {
        super(accountName, storage);
    }

    @Override
    public void deposit(double amount) {
        // Business logic for depositing into checking account
        double currentBalance = storage.retrieveAccountBalance(accountName);
        double newBalance = currentBalance + amount;
        storage.storeAccount(accountName, newBalance);
        System.out.println(amount + " deposited into Checking Account " + accountName);
    }

    @Override
    public void withdraw(double amount) {
        // Business logic for withdrawing from checking account
        double currentBalance = storage.retrieveAccountBalance(accountName);
        if (currentBalance >= amount) {
            double newBalance = currentBalance - amount;
            storage.storeAccount(accountName, newBalance);
            System.out.println(amount + " withdrawn from Checking Account " + accountName);
        } else {
            System.out.println("Insufficient balance in Checking Account " + accountName);
        }
    }

    @Override
    public double getBalance() {
        // Business logic to get balance of checking account
        return storage.retrieveAccountBalance(accountName);
    }
}

// Client Code
public class BridgePatternDemo {
    public static void main(String[] args) {
        // Using FileStorage for SavingsAccount
        Storage fileStorage = new FileStorage();
        Account savingsAccount = new SavingsAccount("Savings-001", fileStorage);
        savingsAccount.deposit(1000);
        savingsAccount.withdraw(500);
        double savingsBalance = savingsAccount.getBalance();
        System.out.println("Savings Account Balance: " + savingsBalance);

        // Using DatabaseStorage for CheckingAccount
        Storage dbStorage = new DatabaseStorage();
        Account checkingAccount = new CheckingAccount("Checking-001", dbStorage);
        checkingAccount.deposit(1500);
        checkingAccount.withdraw(700);
        double checkingBalance = checkingAccount.getBalance();
        System.out.println("Checking Account Balance: " + checkingBalance);
    }
}

/*
C:\>javac BridgePatternDemo.java

C:\>java BridgePatternDemo
Retrieving balance of account Savings-001 from file.
Storing account Savings-001 with balance 2000.0 to file.
1000.0 deposited into Savings Account Savings-001
Retrieving balance of account Savings-001 from file.
Storing account Savings-001 with balance 500.0 to file.
500.0 withdrawn from Savings Account Savings-001
Retrieving balance of account Savings-001 from file.
Savings Account Balance: 1000.0
Retrieving balance of account Checking-001 from database.
Storing account Checking-001 with balance 6500.0 to database.
1500.0 deposited into Checking Account Checking-001
Retrieving balance of account Checking-001 from database.
Storing account Checking-001 with balance 4300.0 to database.
700.0 withdrawn from Checking Account Checking-001
Retrieving balance of account Checking-001 from database.
Checking Account Balance: 5000.0
*/

Pros

  • Decoupling: Separates abstraction from implementation, allowing independent changes.
  • Flexibility: Easily swap or add new implementations without modifying the abstraction.
  • Extensibility: Supports adding new abstractions or implementations without altering existing code.
  • Single Responsibility: Abstraction and implementation have distinct roles.

Cons

  • Complexity: Adds layers of abstraction, which can make the code harder to understand.
  • Overhead: May introduce slight performance overhead due to delegation.
  • Design Effort: Requires careful planning to identify abstractions and implementations.

When to Use

  • When you want to separate an abstraction from its implementation so they can vary independently.
  • When you need to support multiple platforms or implementations (e.g., different databases, rendering engines).
  • When you anticipate changes in either the abstraction or implementation.
  • When you want to avoid a permanent binding between an abstraction and its implementation.

Real-World Example

  • GUI Frameworks: A window (abstraction) can be rendered on different platforms (Windows, macOS) using platform-specific rendering (implementors).
  • JDBC Drivers: The JDBC API (abstraction) works with different database implementations (MySQL, PostgreSQL) via specific drivers (implementors).
  • Device Drivers: A printer interface (abstraction) supports different printer models (implementors).
The Bridge Pattern is a powerful structural design pattern that promotes flexibility by decoupling an abstraction from its implementation, allowing both to evolve independently. It shines in scenarios where you need to support multiple implementations (e.g., rendering methods, database drivers) or anticipate future changes in either the abstraction or implementation. By using composition to link the abstraction to its implementor, it ensures loose coupling, extensibility, and adherence to the single responsibility principle.
Scroll to Top