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
- Abstraction: Defines the abstract interface and holds a reference to the Implementor.
- Refined Abstraction: Extends the Abstraction, providing additional or specific functionality.
- Implementor: Defines the interface for implementation classes (often an interface or abstract class).
- 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).