Mastering Design Patterns in C# and ASP.NET Core
Part 3.2 – Bridge Design Pattern
Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Structural Design Pattern
Difficulty: ⭐⭐⭐⭐☆ (Intermediate)
Prerequisites: OOP Concepts, Interfaces, Abstract Classes, Composition, Dependency Injection
Table of Contents
Introduction
What is the Bridge Design Pattern?
Why Do We Need the Bridge Pattern?
The Problem with Inheritance
Real-World Analogy
Bridge Pattern Structure
UML Class Diagram
Components of the Bridge Pattern
Complete C# Console Application
ASP.NET Core Implementation
Real-World Examples
Advantages
Disadvantages
Best Practices
Common Mistakes
Bridge vs Adapter vs Strategy
Interview Questions
Summary
Introduction
Inheritance is one of the most fundamental concepts in Object-Oriented Programming (OOP). It allows us to reuse code and create specialized classes from existing ones.
However, as applications grow, relying solely on inheritance can lead to an explosion of classes.
Imagine you're building a notification system that supports:
Notification Types
Email
SMS
Push Notification
Providers
Azure Communication Services
Twilio
SendGrid
If you use inheritance for every combination, you might end up with classes like:
AzureEmailNotification
AzureSmsNotification
AzurePushNotification
TwilioEmailNotification
TwilioSmsNotification
TwilioPushNotification
SendGridEmailNotification
SendGridSmsNotification
SendGridPushNotification
With just 3 notification types and 3 providers, you've already created 9 classes.
If tomorrow another provider is introduced, the number of classes increases significantly.
The Bridge Pattern solves this problem by separating the abstraction from its implementation.
What is the Bridge Design Pattern?
Definition
The Bridge Pattern is a Structural Design Pattern that separates an abstraction from its implementation so that both can evolve independently.
Instead of combining every abstraction with every implementation through inheritance, the Bridge Pattern connects them using composition.
This significantly reduces the number of classes and improves flexibility.
Why Do We Need the Bridge Pattern?
Suppose your application supports multiple report types:
Sales Report
Employee Report
Inventory Report
Each report can be exported as:
PDF
Excel
Word
Without the Bridge Pattern, you might create:
SalesPdfReport
SalesExcelReport
SalesWordReport
EmployeePdfReport
EmployeeExcelReport
EmployeeWordReport
InventoryPdfReport
InventoryExcelReport
InventoryWordReport
As report types or export formats grow, the number of classes increases rapidly.
Instead, we can separate:
Report (Abstraction)
Exporter (Implementation)
Any report can now work with any exporter at runtime.
The Problem with Inheritance
Consider two independent dimensions:
Notification Type
Provider
If each notification type inherits from each provider, the design becomes rigid.
Problems include:
Too many classes
Difficult maintenance
Code duplication
Limited flexibility
Violates the principle of Composition over Inheritance
The Bridge Pattern addresses this by connecting abstractions and implementations through interfaces.
Real-World Analogy
Think of a television and its remote control.
Different TV brands:
Samsung
Sony
LG
Different remote types:
Basic Remote
Smart Remote
Voice Remote
You don't create separate remotes for every TV model.
Instead:
The remote (abstraction) communicates with the TV (implementation).
Either the remote or the TV can change independently.
This is the essence of the Bridge Pattern.
Bridge Pattern Structure
The Bridge Pattern consists of:
Abstraction
Refined Abstraction
Implementor
Concrete Implementor
The abstraction holds a reference to the implementor instead of inheriting from it.
UML Class Diagram
+----------------------+
| IExporter |
+----------------------+
| + Export(string) |
+----------^-----------+
|
+----------------+----------------+
| |
+----------------------+ +----------------------+
| PdfExporter | | ExcelExporter |
+----------------------+ +----------------------+
| Export() | | Export() |
+----------------------+ +----------------------+
Composition
▲
|
+----------------------+
| Report |
+----------------------+
| - exporter |
| + Generate() |
+----------^-----------+
|
+------------+-------------+
| |
+----------------------+ +----------------------+
| SalesReport | | EmployeeReport |
+----------------------+ +----------------------+
| Generate() | | Generate() |
+----------------------+ +----------------------+
Components of the Bridge Pattern
Implementor
Defines the implementation interface.
public interface IExporter
{
void Export(string data);
}
Concrete Implementors
public class PdfExporter : IExporter
{
public void Export(string data)
{
Console.WriteLine($"Exporting PDF: {data}");
}
}
public class ExcelExporter : IExporter
{
public void Export(string data)
{
Console.WriteLine($"Exporting Excel: {data}");
}
}
Abstraction
public abstract class Report
{
protected readonly IExporter exporter;
protected Report(IExporter exporter)
{
this.exporter = exporter;
}
public abstract void Generate();
}
Refined Abstraction
public class SalesReport : Report
{
public SalesReport(IExporter exporter)
: base(exporter)
{
}
public override void Generate()
{
exporter.Export("Sales Report");
}
}
Complete C# Console Application
class Program
{
static void Main()
{
Report report = new SalesReport(new PdfExporter());
report.Generate();
report = new SalesReport(new ExcelExporter());
report.Generate();
}
}
Output
Exporting PDF: Sales Report
Exporting Excel: Sales Report
Notice that the SalesReport class never changes.
Only the exporter changes.
ASP.NET Core Implementation
Suppose an ASP.NET Core application generates invoices.
Invoices can be exported to:
PDF
Excel
Export Interface
public interface IInvoiceExporter
{
void Export(string invoice);
}
Exporters
public class PdfInvoiceExporter : IInvoiceExporter
{
public void Export(string invoice)
{
Console.WriteLine($"PDF Invoice: {invoice}");
}
}
public class ExcelInvoiceExporter : IInvoiceExporter
{
public void Export(string invoice)
{
Console.WriteLine($"Excel Invoice: {invoice}");
}
}
Invoice Service
public class InvoiceService
{
private readonly IInvoiceExporter _exporter;
public InvoiceService(IInvoiceExporter exporter)
{
_exporter = exporter;
}
public void GenerateInvoice()
{
_exporter.Export("Invoice #1001");
}
}
Dependency Injection
builder.Services.AddScoped<IInvoiceExporter, PdfInvoiceExporter>();
builder.Services.AddScoped<InvoiceService>();
Changing the registration to:
builder.Services.AddScoped<IInvoiceExporter, ExcelInvoiceExporter>();
changes the implementation without modifying the business logic.
Real-World Examples
The Bridge Pattern is commonly used when two dimensions of variation should evolve independently.
Document Generation
Reports:
Sales
Employee
Inventory
Formats:
PDF
Excel
Word
Notification Systems
Notifications:
Email
SMS
Push
Providers:
Azure Communication Services
Twilio
SendGrid
Payment Processing
Payment Types:
Card
UPI
Wallet
Providers:
Stripe
Razorpay
PayPal
Logging Systems
Log Types:
File
Database
Cloud
Frameworks:
Serilog
NLog
Microsoft Logging
Cloud Storage
Storage Operations:
Upload
Download
Delete
Providers:
Azure Blob Storage
Amazon S3
Google Cloud Storage
Advantages
Reduces class explosion.
Separates abstraction from implementation.
Promotes composition over inheritance.
Supports the Open/Closed Principle.
Improves maintainability.
Allows runtime switching of implementations.
Encourages loose coupling.
Disadvantages
Introduces additional abstractions and interfaces.
Can increase the number of classes for simple applications.
Requires careful design to identify independent dimensions.
Best Practices
Use the Bridge Pattern when there are two or more independent dimensions of variation.
Prefer composition instead of inheritance.
Inject implementors using Dependency Injection.
Keep abstractions focused on business behavior and implementors focused on technical details.
Avoid adding business logic to implementor classes.
Common Mistakes
Confusing Bridge with Adapter
The Adapter Pattern makes incompatible interfaces work together.
The Bridge Pattern separates abstraction from implementation.
Overusing Inheritance
If you're creating many subclasses just to support combinations of features, consider using the Bridge Pattern instead.
Mixing Responsibilities
Keep the abstraction responsible for business operations and the implementor responsible for execution details.
Using Bridge for Simple Designs
If there is only one implementation and no expectation of future variation, the Bridge Pattern may add unnecessary complexity.
Bridge vs Adapter vs Strategy
| Feature | Bridge | Adapter | Strategy |
|---|---|---|---|
| Purpose | Separate abstraction and implementation | Convert incompatible interfaces | Encapsulate interchangeable algorithms |
| Uses Composition | ✅ Yes | ✅ Yes | ✅ Yes |
| Changes Interface | ❌ No | ✅ Yes | ❌ No |
| Runtime Flexibility | High | Moderate | High |
| Primary Goal | Avoid class explosion | Integration | Algorithm selection |
Interview Questions
1. What is the Bridge Design Pattern?
A structural design pattern that separates an abstraction from its implementation so that both can evolve independently.
2. What problem does the Bridge Pattern solve?
It prevents a combinatorial explosion of subclasses when there are multiple independent dimensions of variation.
3. What is the difference between Bridge and Adapter?
Bridge is designed during system architecture to separate abstractions from implementations.
Adapter is typically introduced later to make incompatible interfaces work together.
4. Which SOLID principles does the Bridge Pattern support?
Open/Closed Principle (OCP): New abstractions and implementations can be added without modifying existing code.
Dependency Inversion Principle (DIP): Abstractions depend on interfaces rather than concrete implementations.
5. Where is the Bridge Pattern commonly used?
Report generation
Notification systems
Payment providers
Logging frameworks
Cloud storage abstractions
Cross-platform applications
6. Why is composition preferred over inheritance in the Bridge Pattern?
Composition provides greater flexibility, reduces coupling, and allows implementations to be changed at runtime without modifying the abstraction.
Summary
The Bridge Design Pattern is a powerful structural pattern that separates what an object does (abstraction) from how it does it (implementation). By connecting the two through composition instead of inheritance, it prevents class explosion, improves flexibility, and supports scalable application design.
In modern C# and ASP.NET Core applications, the Bridge Pattern is particularly valuable when integrating multiple providers, supporting different output formats, or designing systems with independently evolving dimensions.
Mastering the Bridge Pattern will help you create cleaner architectures that are easier to extend, maintain, and test as business requirements grow.
Coming Up Next: Part 3.3 – Composite Design Pattern
In the next article, we'll explore the Composite Design Pattern, including:
What is the Composite Pattern?
Tree Structures and Hierarchical Objects
Parent–Child Relationships
Leaf vs. Composite Objects
UML Class Diagram
Complete C# Console Application
ASP.NET Core implementation
Real-world examples (File System, Organization Hierarchy, Menu Systems)
Advantages and disadvantages
Best practices
Common mistakes
Interview questions
You'll learn how the Composite Pattern enables you to treat individual objects and groups of objects uniformly, making it ideal for representing hierarchical structures such as folders, menus, organizational charts, and UI components.
No comments:
Post a Comment