Bridge
Splits a large class or set of closely related classes into two separate hierarchies.
Bridge Pattern: A Complete Guide with Examples in 11 Programming Languages
The Bridge Pattern is a structural design pattern that decouples an abstraction from its implementation so that the two can vary independently. This pattern involves an interface that acts as a bridge between the abstraction class and implementer classes, allowing you to change both without affecting each other. Whether you're building cross-platform applications, supporting multiple database types, or managing different rendering engines, the Bridge Pattern provides the flexibility to evolve your abstractions and implementations independently.
In this comprehensive guide, we'll explore the Bridge Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin - complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is the Bridge Pattern?
- Why Use the Bridge Pattern?
- Bridge Pattern Comparison
- Bridge Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Bridge Pattern?
The Bridge Pattern separates an object's abstraction from its implementation by creating two separate class hierarchies. The abstraction contains a reference to the implementation, forming a "bridge" between them. This allows both hierarchies to change independently without affecting each other.
Think of it like a TV and a remote control. The remote (abstraction) can control any TV (implementation) through a standard interface. You can change the TV brand without changing the remote, or upgrade the remote without replacing the TV. The "bridge" is the interface between them.
Why Use the Bridge Pattern?
The Bridge Pattern offers several compelling benefits:
- Decoupling: Separates interface from implementation
- Flexibility: Both abstraction and implementation can vary independently
- Extensibility: Add new abstractions or implementations without modifying existing code
- Reduced Class Explosion: Avoids creating a class for every abstraction-implementation combination
- Runtime Binding: Switch implementations at runtime
- Platform Independence: Write platform-agnostic abstractions with platform-specific implementations
- Single Responsibility: Each class focuses on either abstraction or implementation
Bridge Pattern Comparison
Let's compare Bridge with related patterns:
| Pattern | Intent | Hierarchies | When Designed | Primary Goal |
|---|---|---|---|---|
| Bridge | Decouple abstraction from implementation | Two separate | Design time (before building) | Separate concerns for independent variation |
| Adapter | Make incompatible interfaces work together | One | Runtime (after building) | Interface compatibility |
| Strategy | Encapsulate algorithms | One (algorithms) | Runtime | Swap algorithms/behaviors |
| Abstract Factory | Create families of objects | Multiple (products) | Design time | Create related objects |
| Decorator | Add responsibilities dynamically | One (decorators) | Runtime | Enhance behavior |
Key Distinction: Bridge is designed upfront for independent variation; Adapter is retrofitted for compatibility.
Bridge Pattern Explained
The Bridge Pattern typically involves:
Core Components:
- Abstraction: Defines the abstraction's interface and maintains a reference to an Implementor
- Refined Abstraction: Extends the interface defined by Abstraction
- Implementor: Defines the interface for implementation classes (not necessarily matching Abstraction's interface)
- Concrete Implementor: Implements the Implementor interface
Key Relationships:
- Abstraction contains (has-a) an Implementor reference
- Abstraction delegates to Implementor rather than implementing directly
- Client works through Abstraction, unaware of Implementor details
Common Use Cases:
- Cross-platform UI frameworks
- Database drivers and connection types
- Graphics rendering systems
- Device drivers and operating systems
- Messaging systems with multiple transport protocols
Class Diagram
Here's the UML class diagram showing the Bridge structure:
Beginner-Friendly Example
Let's start with a simple example: a shape drawing system that can render shapes using different rendering engines.
from abc import ABC, abstractmethod
from typing import Protocol
# Implementor interface
class Renderer(Protocol):
"""Interface for rendering implementations"""
def render_circle(self, radius: float) -> None:
...
def render_square(self, side: float) -> None:
...
# Concrete Implementors
class VectorRenderer:
"""Renders shapes using vector graphics"""
def render_circle(self, radius: float) -> None:
print(f"Drawing a circle of radius {radius} with vector graphics")
def render_square(self, side: float) -> None:
print(f"Drawing a square of side {side} with vector graphics")
class RasterRenderer:
"""Renders shapes using raster graphics"""
def render_circle(self, radius: float) -> None:
print(f"Drawing pixels for circle with radius {radius}")
def render_square(self, side: float) -> None:
print(f"Drawing pixels for square with side {side}")
# Abstraction
class Shape(ABC):
"""Base abstraction for shapes"""
def __init__(self, renderer: Renderer):
self.renderer = renderer
@abstractmethod
def draw(self) -> None:
pass
@abstractmethod
def resize(self, factor: float) -> None:
pass
# Refined Abstractions
class Circle(Shape):
"""Circle shape abstraction"""
def __init__(self, renderer: Renderer, radius: float):
super().__init__(renderer)
self.radius = radius
def draw(self) -> None:
self.renderer.render_circle(self.radius)
def resize(self, factor: float) -> None:
self.radius *= factor
class Square(Shape):
"""Square shape abstraction"""
def __init__(self, renderer: Renderer, side: float):
super().__init__(renderer)
self.side = side
def draw(self) -> None:
self.renderer.render_square(self.side)
def resize(self, factor: float) -> None:
self.side *= factor
# Usage
if __name__ == "__main__":
# Create renderers
vector = VectorRenderer()
raster = RasterRenderer()
# Create shapes with different renderers
circle1 = Circle(vector, 5)
circle2 = Circle(raster, 5)
square1 = Square(vector, 10)
square2 = Square(raster, 10)
# Draw shapes
print("Vector renderer:")
circle1.draw() # Drawing a circle of radius 5 with vector graphics
square1.draw() # Drawing a square of side 10 with vector graphics
print("\nRaster renderer:")
circle2.draw() # Drawing pixels for circle with radius 5
square2.draw() # Drawing pixels for square with side 10
# Resize and redraw
print("\nAfter resize:")
circle1.resize(2)
circle1.draw() # Drawing a circle of radius 10 with vector graphics
Production-Ready Example
Now let's look at a more realistic example: a cross-platform notification system that supports different notification types and delivery platforms.
š Python
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, List
from enum import Enum
# Implementor interface
class NotificationSender(ABC):
"""Interface for notification delivery implementations"""
@abstractmethod
def send(self, recipient: str, title: str, message: str, metadata: Dict) -> bool:
pass
@abstractmethod
def validate_recipient(self, recipient: str) -> bool:
pass
# Concrete Implementors
class EmailSender(NotificationSender):
"""Sends notifications via email"""
def __init__(self, smtp_server: str, port: int):
self.smtp_server = smtp_server
self.port = port
def send(self, recipient: str, title: str, message: str, metadata: Dict) -> bool:
print(f"[EMAIL] Connecting to {self.smtp_server}:{self.port}")
print(f"[EMAIL] To: {recipient}")
print(f"[EMAIL] Subject: {title}")
print(f"[EMAIL] Body: {message}")
print(f"[EMAIL] Sent successfully at {datetime.now()}")
return True
def validate_recipient(self, recipient: str) -> bool:
return "@" in recipient and "." in recipient.split("@")[1]
class SMSSender(NotificationSender):
"""Sends notifications via SMS"""
def __init__(self, api_key: str, gateway: str):
self.api_key = api_key
self.gateway = gateway
def send(self, recipient: str, title: str, message: str, metadata: Dict) -> bool:
# SMS combines title and message due to length constraints
sms_text = f"{title}: {message}"
print(f"[SMS] Using gateway: {self.gateway}")
print(f"[SMS] To: {recipient}")
print(f"[SMS] Message: {sms_text[:160]}") # SMS length limit
print(f"[SMS] Sent successfully at {datetime.now()}")
return True
def validate_recipient(self, recipient: str) -> bool:
# Simple phone number validation
cleaned = recipient.replace("+", "").replace("-", "").replace(" ", "")
return cleaned.isdigit() and 10 <= len(cleaned) <= 15
class PushNotificationSender(NotificationSender):
"""Sends push notifications to mobile devices"""
def __init__(self, app_id: str, api_endpoint: str):
self.app_id = app_id
self.api_endpoint = api_endpoint
def send(self, recipient: str, title: str, message: str, metadata: Dict) -> bool:
print(f"[PUSH] Using app: {self.app_id}")
print(f"[PUSH] Endpoint: {self.api_endpoint}")
print(f"[PUSH] Device token: {recipient}")
print(f"[PUSH] Title: {title}")
print(f"[PUSH] Message: {message}")
if metadata:
print(f"[PUSH] Metadata: {metadata}")
print(f"[PUSH] Sent successfully at {datetime.now()}")
return True
def validate_recipient(self, recipient: str) -> bool:
# Simple device token validation (would be more complex in reality)
return len(recipient) >= 32
# Abstraction
class Notification(ABC):
"""Base abstraction for notifications"""
def __init__(self, sender: NotificationSender):
self.sender = sender
self.timestamp = datetime.now()
@abstractmethod
def send(self, recipient: str) -> bool:
pass
def can_send_to(self, recipient: str) -> bool:
"""Check if notification can be sent to recipient"""
return self.sender.validate_recipient(recipient)
# Refined Abstractions
class AlertNotification(Notification):
"""High-priority alert notifications"""
def __init__(self, sender: NotificationSender, alert_level: str, message: str):
super().__init__(sender)
self.alert_level = alert_level
self.message = message
def send(self, recipient: str) -> bool:
if not self.can_send_to(recipient):
print(f"Invalid recipient: {recipient}")
return False
title = f"šØ ALERT [{self.alert_level}]"
metadata = {
"priority": "high",
"alert_level": self.alert_level,
"timestamp": self.timestamp.isoformat()
}
return self.sender.send(recipient, title, self.message, metadata)
class PromotionalNotification(Notification):
"""Marketing and promotional notifications"""
def __init__(self, sender: NotificationSender, campaign_id: str,
offer: str, cta_text: str):
super().__init__(sender)
self.campaign_id = campaign_id
self.offer = offer
self.cta_text = cta_text
def send(self, recipient: str) -> bool:
if not self.can_send_to(recipient):
print(f"Invalid recipient: {recipient}")
return False
title = "š Special Offer"
message = f"{self.offer}\n\n{self.cta_text}"
metadata = {
"priority": "low",
"campaign_id": self.campaign_id,
"type": "promotional"
}
return self.sender.send(recipient, title, message, metadata)
class TransactionalNotification(Notification):
"""Transaction and order notifications"""
def __init__(self, sender: NotificationSender, transaction_id: str,
amount: float, status: str):
super().__init__(sender)
self.transaction_id = transaction_id
self.amount = amount
self.status = status
def send(self, recipient: str) -> bool:
if not self.can_send_to(recipient):
print(f"Invalid recipient: {recipient}")
return False
title = f"Transaction {self.status}"
message = f"Transaction ID: {self.transaction_id}\n" \
f"Amount: ${self.amount:.2f}\n" \
f"Status: {self.status}"
metadata = {
"priority": "medium",
"transaction_id": self.transaction_id,
"amount": self.amount
}
return self.sender.send(recipient, title, message, metadata)
# Usage and Testing
if __name__ == "__main__":
print("=== Production Notification System ===\n")
# Create senders (implementations)
email_sender = EmailSender("smtp.example.com", 587)
sms_sender = SMSSender("api-key-12345", "sms-gateway.com")
push_sender = PushNotificationSender("app-123", "https://push.api.com")
# Create notifications (abstractions) with different senders
print("--- Alert via Email ---")
alert_email = AlertNotification(email_sender, "CRITICAL",
"Server CPU usage above 90%")
alert_email.send("admin@example.com")
print("\n--- Alert via SMS ---")
alert_sms = AlertNotification(sms_sender, "WARNING",
"Database backup failed")
alert_sms.send("+1-555-0123")
print("\n--- Promotional via Push ---")
promo_push = PromotionalNotification(
push_sender,
campaign_id="SUMMER2024",
offer="Get 50% off on all items!",
cta_text="Shop Now"
)
promo_push.send("device-token-abc123def456ghi789")
print("\n--- Transactional via Email ---")
transaction_email = TransactionalNotification(
email_sender,
transaction_id="TXN-78901",
amount=149.99,
status="Completed"
)
transaction_email.send("customer@example.com")
# Demonstrate flexibility: same notification, different senders
print("\n--- Same Transaction via SMS ---")
transaction_sms = TransactionalNotification(
sms_sender,
transaction_id="TXN-78902",
amount=249.99,
status="Pending"
)
transaction_sms.send("+1-555-0456")
This production example demonstrates:
- Multiple abstraction types (Alert, Promotional, Transactional)
- Multiple implementation types (Email, SMS, Push)
- Validation logic
- Metadata handling
- Priority levels
- Real-world notification scenarios
Real-World Use Cases
The Bridge Pattern is particularly useful in these scenarios:
1. Cross-Platform UI Frameworks
Different UI widgets (Button, TextField, Window) can render on different platforms (Windows, macOS, Linux):
# Abstraction: UI Widgets
class Button:
def __init__(self, platform_renderer):
self.renderer = platform_renderer
def render(self):
self.renderer.render_button()
# Implementations: Platform Renderers
class WindowsRenderer:
def render_button(self):
# Windows-specific rendering
class MacOSRenderer:
def render_button(self):
# macOS-specific rendering
2. Database Abstraction Layers
Different database operations (QueryBuilder, Transaction) can work with different database types (MySQL, PostgreSQL, MongoDB):
# Abstraction: Database Operations
class QueryBuilder:
def __init__(self, database_driver):
self.driver = database_driver
def select(self, table):
return self.driver.execute_select(table)
# Implementations: Database Drivers
class MySQLDriver:
def execute_select(self, table):
# MySQL-specific query syntax
class PostgreSQLDriver:
def execute_select(self, table):
# PostgreSQL-specific query syntax
3. Payment Processing
Different payment types (CreditCard, PayPal, Cryptocurrency) can use different payment gateways (Stripe, Square, PayPal API):
# Abstraction: Payment Methods
class CreditCardPayment:
def __init__(self, gateway):
self.gateway = gateway
def process(self, amount):
return self.gateway.charge(amount, "credit_card")
# Implementations: Payment Gateways
class StripeGateway:
def charge(self, amount, method):
# Stripe API call
class SquareGateway:
def charge(self, amount, method):
# Square API call
4. Logging Systems
Different log levels (Debug, Info, Error) can write to different destinations (File, Database, Cloud):
# Abstraction: Log Levels
class ErrorLog:
def __init__(self, log_writer):
self.writer = log_writer
def log(self, message):
formatted = f"[ERROR] {message}"
self.writer.write(formatted)
# Implementations: Log Writers
class FileLogWriter:
def write(self, message):
# Write to file
class CloudLogWriter:
def write(self, message):
# Send to cloud logging service
5. Message Queue Systems
Different message types (Command, Event, Query) can be sent through different brokers (RabbitMQ, Kafka, Redis):
# Abstraction: Message Types
class EventMessage:
def __init__(self, broker):
self.broker = broker
def publish(self, event_data):
return self.broker.send(event_data, "events")
# Implementations: Message Brokers
class KafkaBroker:
def send(self, data, topic):
# Kafka-specific publish
class RabbitMQBroker:
def send(self, data, queue):
# RabbitMQ-specific publish
Language-Specific Mistakes and Anti-Patterns
ā Anti-Pattern: Tight Coupling via Inheritance
# BAD: Shape inherits from VectorRenderer
class Circle(VectorRenderer):
def draw(self):
self.render_circle(5)
# Now changing rendering requires changing shape hierarchy
ā Solution: Composition Over Inheritance
# GOOD: Shape composes Renderer
class Circle:
def __init__(self, renderer):
self.renderer = renderer
def draw(self):
self.renderer.render_circle(5)
ā Anti-Pattern: Not Using Protocols/ABCs
# BAD: No formal interface
class Shape:
def __init__(self, renderer):
self.renderer = renderer # Any object accepted
ā Solution: Use Protocols for Duck Typing
# GOOD: Explicit interface
from typing import Protocol
class Renderer(Protocol):
def render_circle(self, radius: float) -> None: ...
Frequently Asked Questions
Q1: When should I use Bridge vs Adapter?
Bridge is designed before the system is built:
- You anticipate both abstraction and implementation will vary
- You want to avoid class explosion
- You're designing for extensibility from the start
Adapter is applied after the system exists:
- You need to integrate incompatible interfaces
- You're working with legacy code
- It's a retrofit solution
Example distinction:
# Bridge (designed upfront)
class Shape:
def __init__(self, renderer): # Design decision
self.renderer = renderer
# Adapter (retrofit)
class LegacyPrinterAdapter:
def __init__(self, legacy_printer): # Making incompatible code work
self.adaptee = legacy_printer
Q2: Isn't Bridge just dependency injection?
Not quite, but they're related:
Bridge Pattern:
- Design pattern (architectural decision)
- Focuses on separating two dimensions of variation
- Creates two hierarchies that can vary independently
- Technique for providing dependencies
- Can be used to implement Bridge
- More general-purpose than Bridge
They work together:
# Bridge pattern structure
class Shape:
def __init__(self, renderer): # DI helps inject the renderer
self.renderer = renderer # Bridge separates shape from rendering
# Bridge is the "what" (pattern), DI is the "how" (technique)
Q3: Can I have multiple implementations per abstraction?
Yes! That's the flexibility of Bridge:
class AdvancedShape(Shape):
def __init__(self, primary_renderer, fallback_renderer):
super().__init__(primary_renderer)
self.fallback = fallback_renderer
def draw(self):
try:
super().draw()
except Exception:
# Fallback to secondary renderer
self.renderer = self.fallback
super().draw()
Q4: How do I handle renderer-specific features?
Use the Liskov Substitution Principle - only use common interface:
# BAD: Abstraction depends on specific implementation feature
class Shape:
def draw(self):
if isinstance(self.renderer, VectorRenderer):
self.renderer.set_antialiasing(True) # Only VectorRenderer has this
self.renderer.render()
# GOOD: Keep abstractions independent
class Shape:
def draw(self):
self.renderer.render() # Common interface only
# If you need special features, use a different abstraction
class AntialiasedShape(Shape):
def draw(self):
# This abstraction requires renderers supporting antialiasing
self.renderer.set_quality_settings()
super().draw()
Q5: Should I use Bridge for two variations or wait for more?
General guideline:
Use Bridge when:
- ā You have 2+ abstractions and 2+ implementations
- ā Both dimensions are likely to grow independently
- ā Avoiding class explosion is important
Don't use Bridge when:
- ā Only one abstraction or one implementation
- ā Variations are unlikely to grow
- ā Simple inheritance would suffice
Example calculation:
Without Bridge:
3 shapes Ć 2 renderers = 6 classes
With Bridge:
3 shapes + 2 renderers = 5 classes (better already)
As it grows:
Without: 10 shapes Ć 5 renderers = 50 classes
With: 10 shapes + 5 renderers = 15 classes (huge savings)
Q6: How does Bridge relate to Strategy pattern?
They're similar but with different intents:
| Aspect | Bridge | Strategy |
|---|---|---|
| Intent | Separate abstraction from implementation | Encapsulate interchangeable algorithms |
| Hierarchies | Two separate hierarchies | One algorithm hierarchy |
| Focus | Structure (how it's built) | Behavior (how it works) |
| Client awareness | Client doesn't know implementation | Client often chooses strategy |
| When decided | Design time | Often runtime |
Example:
# Bridge: Structure-focused
class Shape:
def __init__(self, renderer): # How shape is rendered
self.renderer = renderer
# Strategy: Behavior-focused
class Sorter:
def __init__(self, strategy): # Which algorithm to use
self.strategy = strategy
Q7: Can abstractions share implementations?
Yes! That's a key benefit:
# Multiple abstractions can share the same implementation
vector = VectorRenderer()
circle = Circle(vector, 5)
square = Square(vector, 10)
triangle = Triangle(vector, 3, 4, 5)
# All use the same renderer instance
Q8: How do I test Bridge pattern code?
Testing is easier with Bridge because of separation:
# Test abstractions with mock implementations
class MockRenderer:
def __init__(self):
self.render_calls = []
def render_circle(self, radius):
self.render_calls.append(('circle', radius))
def test_circle_draw():
mock = MockRenderer()
circle = Circle(mock, 5)
circle.draw()
assert mock.render_calls == [('circle', 5)]
# Test implementations independently
def test_vector_renderer():
renderer = VectorRenderer()
# Test rendering logic without shapes
renderer.render_circle(10)
Q9: Should Renderer be stateless or stateful?
Depends on your use case, but stateless is often better:
Stateless (preferred):
class VectorRenderer:
def render_circle(self, radius):
# No internal state, pure function
return self._create_svg_circle(radius)
# Can be shared safely
shared_renderer = VectorRenderer()
circle1 = Circle(shared_renderer, 5)
circle2 = Circle(shared_renderer, 10) # Safe to share
Stateful (when needed):
class BatchRenderer:
def __init__(self):
self.batch = [] # State: accumulated operations
def render_circle(self, radius):
self.batch.append(('circle', radius))
def flush(self):
# Render all batched operations
for operation in self.batch:
self._execute(operation)
self.batch.clear()
# Each abstraction needs its own renderer
circle = Circle(BatchRenderer(), 5) # Own instance
Q10: How do I handle renderer initialization/cleanup?
Use context managers or disposal patterns:
# Python: Context manager
class DatabaseRenderer:
def __enter__(self):
self.connection = connect_to_db()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.close()
with DatabaseRenderer() as renderer:
shape = Circle(renderer, 5)
shape.draw()
# Automatic cleanup
# C#: IDisposable
public class DatabaseRenderer : IRenderer, IDisposable
{
private DbConnection connection;
public void Dispose()
{
connection?.Dispose();
}
}
using (var renderer = new DatabaseRenderer())
{
var shape = new Circle(renderer, 5);
shape.Draw();
} // Automatic disposal
Key Takeaways
Let's wrap up what we've learned about the Bridge Pattern across multiple programming languages:
šÆ Core Concept: Bridge decouples abstraction from implementation, allowing both to vary independently. It creates two separate class hierarchies connected through composition rather than inheritance.
š Key Characteristics:
- Separation of Concerns: Abstraction and implementation in separate hierarchies
- Composition Over Inheritance: Abstraction contains implementation reference
- Independent Variation: Change abstractions or implementations without affecting each other
- Runtime Flexibility: Can switch implementations at runtime
š Language-Specific Considerations:
- Python: Use Protocols or ABCs for interfaces; prefer composition
- Java: Use interfaces with dependency injection; make renderer final
- C#: Use interfaces with null validation; consider IDisposable for cleanup
- JavaScript/TypeScript: Use interfaces (TS) or duck typing (JS); leverage modules
- Kotlin: Use interfaces; sealed classes for known implementations
- Go: Use interface{}; verify compliance with var _ Interface = (*Type)(nil)
- Rust: Use trait objects (Box<dyn Trait>) or generics; handle ownership carefully
- Swift: Use protocols; prefer structs for implementations
- Dart: Use abstract classes; factory constructors for creation
- PHP: Use interfaces; consider traits for shared behavior
š When to Use Bridge:
- Cross-platform applications (UI, databases, file systems)
- Graphics/rendering systems with multiple backends
- Device drivers with multiple OS platforms
- Messaging systems with multiple transport protocols
- Database abstraction layers
- Payment processing with multiple gateways
- Logging systems with multiple destinations
ā ļø When NOT to Use Bridge:
- Only one abstraction or one implementation
- Variations unlikely to grow
- Simple scenarios where inheritance suffices
- Performance-critical code where indirection is costly
- When Adapter pattern is more appropriate (retrofitting)
š ļø Best Practices Across All Languages:
- Design Upfront: Bridge is a proactive pattern, not reactive
- Keep Interfaces Focused: Implementor doesn't mirror Abstraction's interface
- Use Dependency Injection: Inject implementation into abstraction
- Validate Constructor Parameters: Prevent null/nil implementations
- Program to Interfaces: Never depend on concrete implementation classes
- Keep Implementations Stateless: Prefer immutable, shareable implementations
- Document the Bridge: Make it clear why both hierarchies exist
- Test Independently: Test abstractions and implementations separately
āļø Bridge vs. Related Patterns:
- vs. Adapter: Bridge is design-time, Adapter is runtime; Bridge prevents class explosion, Adapter fixes incompatibility
- vs. Strategy: Bridge is structural, Strategy is behavioral; Bridge has two hierarchies, Strategy has one
- vs. Abstract Factory: Bridge creates structure, Factory creates objects; Bridge focuses on single concern, Factory on families
š” Remember:
- Bridge is about structural separation, not just dependency injection
- Composition over inheritance is the key principle
- Use when you have two dimensions that vary independently
- Prevent class explosion: N abstractions + M implementations instead of N Ć M classes
- It's proactive (designed upfront) vs. Adapter (reactive/retrofitted)
š Common Real-World Applications:
- GUI Frameworks: Widgets (Button, Window) + Platforms (Windows, macOS, Linux)
- Database Layers: Operations (Query, Transaction) + Drivers (MySQL, PostgreSQL, MongoDB)
- Graphics Systems: Shapes (Circle, Rectangle) + Renderers (Vector, Raster, 3D)
- Notification Systems: Types (Alert, Promotional) + Channels (Email, SMS, Push)
- Remote Controls: Controls (Basic, Advanced) + Devices (TV, Radio, AC)
The Bridge Pattern is a powerful structural pattern that prevents the combinatorial explosion of classes when you have multiple varying dimensions. By separating abstraction from implementation through composition, it provides flexibility, extensibility, and maintainability. While it adds complexity through additional interfaces and indirection, the benefits are substantial when both hierarchies need to evolve independently. Modern applications with cross-platform requirements, multiple backend integrations, or varying behavior implementations benefit greatly from the Bridge pattern.
Want to dive deeper into design patterns? Check out our comprehensive guides on:
- Adapter Pattern
- Strategy Pattern
- Abstract Factory Pattern
- Decorator Pattern
- Composite Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.