Bridge Pattern trong C#: Chặn Cơn Bùng Nổ Class 3x3
Bridge pattern trong C# / .NET 10: tách abstraction (style notification) khỏi implementation (channel gửi) để giữ N+M class thay vì N*M.
Mục lục
- Bridge pattern giải quyết bài toán gì trong C#?
- Bùng nổ class hiện ra trong code thật thế nào?
- Cấu trúc Bridge trong .NET 10 trông thế nào?
- Khi nào dùng Bridge thay vì chỉ composition đơn giản?
- So sánh Bridge với Adapter và Strategy thế nào?
- Một ví dụ thật trong C# / .NET 10 trông thế nào?
- Đọc tiếp gì trong series?
Shop của bạn gửi notification. Ba style — Urgent (order fail, dừng dây chuyền), Info (order shipped), Marketing (we miss you). Ba channel — email, SMS, Slack. Dev đầu tiên dây dưa cách hiển nhiên:
EmailUrgent SmsUrgent SlackUrgent
EmailInfo SmsInfo SlackInfo
EmailMarketing SmsMarketing SlackMarketing
Chín class cho hai trục biến đổi. Ngày product hỏi channel thứ tư (Push) và style thứ tư (Onboarding), số nhảy lên mười sáu. Mỗi style mới đòi viết mọi channel; mỗi channel mới đòi viết mọi style. Diff cho "một feature" trải mười hai file.
Bridge pattern là câu trả lời. Style notification và channel gửi là hai trục độc lập; để mỗi cái mọc trên hierarchy riêng và kết hợp tại runtime. Trong C# hiện đại, đây là composition thẳng với DI — nhưng nhận ra hình dạng trước khi có chín class mới là cái cứu codebase.
Bridge pattern giải quyết bài toán gì trong C#?
Pattern xứng chỗ khi một class đã mọc theo hai trục vuông góc và tích chéo đang vượt tầm kiểm soát. Ba hình dạng cụ thể:
- Style notification × channel. Như trên. Ba style, ba channel, dự kiến mọc cả hai trục.
- Style renderer × loại document. Service báo cáo emit PDF, HTML, JSON; mỗi cái có dạng tóm tắt hoặc chi tiết. Sáu renderer hôm nay, mười hai khi CSV tới, mười tám với style executive.
- Theme UI × kit control. Light/dark nhân desktop/mobile nhân high-contrast. Không có Bridge, mỗi theme mới nhân khối lượng theo số kit control.
Cái không phải bài toán Bridge: "Tôi có một class và muốn đổi thuật toán" — đó là Strategy. "Tôi có một SDK ngoài không khớp interface" — đó là Adapter. Bridge đặc biệt nói về hai hierarchy được phép biến đổi độc lập.
Bùng nổ class hiện ra trong code thật thế nào?
Mùi hôi khó nhận ở ba class — EmailUrgent, SmsUrgent,
SlackUrgent trông ổn. Nó hiện rõ ở sáu và không chịu nổi ở chín. Một
chẩn đoán nhanh hơn: mở một class bất kỳ và hỏi "nó thực sự làm gì?".
Nếu câu trả lời là "nó format message khẩn cấp và gửi qua
email", bạn có hai trách nhiệm hàn dính. Từ "và" trong mô tả là
triệu chứng.
Mỗi class trùng lặp phần của hai câu chuyện:
// Trước - mỗi class trùng style + channel logic
public sealed class EmailUrgent
{
private readonly SmtpClient _smtp;
public Task SendAsync(string to, string body)
{
var subject = "🚨 URGENT — action required"; // style logic
var html = $"<h1>{subject}</h1><p>{body}</p>"; // style logic
return _smtp.SendAsync(to, subject, html); // channel logic
}
}
public sealed class SmsUrgent
{
private readonly TwilioClient _twilio;
public Task SendAsync(string to, string body)
{
var prefix = "URGENT: "; // style logic trùng
return _twilio.SendAsync(to, prefix + body); // channel logic
}
}
Logic trang trí "URGENT" giờ sống ở hai chỗ. Khi marketing hỏi thêm emoji còi báo động vào mọi message khẩn, bạn ghé mọi class trong cột khẩn.
Cấu trúc Bridge trong .NET 10 trông thế nào?
Chia hai trách nhiệm theo đường nối giữa style và channel. Style thành abstraction giữ channel; channel thành interface implementation:
// Phía implementation (channel - biết cách gửi)
public interface INotificationChannel
{
Task DeliverAsync(string recipient, string subject, string body, Priority priority,
CancellationToken ct);
}
public sealed class EmailChannel : INotificationChannel { /* SMTP */ }
public sealed class SmsChannel : INotificationChannel { /* Twilio */ }
public sealed class SlackChannel : INotificationChannel { /* Slack webhook */ }
// Phía abstraction (style - biết nói gì)
public abstract class Notification
{
protected INotificationChannel Channel { get; }
protected Notification(INotificationChannel channel) => Channel = channel;
public abstract Task SendAsync(string recipient, string raw, CancellationToken ct);
}
public sealed class UrgentNotification : Notification
{
public UrgentNotification(INotificationChannel ch) : base(ch) { }
public override Task SendAsync(string to, string raw, CancellationToken ct)
=> Channel.DeliverAsync(to, "🚨 URGENT — action required",
$"URGENT: {raw}", Priority.High, ct);
}
public sealed class InfoNotification : Notification
{
public InfoNotification(INotificationChannel ch) : base(ch) { }
public override Task SendAsync(string to, string raw, CancellationToken ct)
=> Channel.DeliverAsync(to, "Update", raw, Priority.Normal, ct);
}
public sealed class MarketingNotification : Notification
{
public MarketingNotification(INotificationChannel ch) : base(ch) { }
public override Task SendAsync(string to, string raw, CancellationToken ct)
=> Channel.DeliverAsync(to, "We thought you'd like to know…",
raw + "\n\nUnsubscribe: …", Priority.Low, ct);
}
Số class giảm từ chín xuống sáu (ba style + ba channel). Thêm style thứ tư là một class mới cộng zero edit channel. Thêm channel thứ tư là một class mới cộng zero edit style. Bức tranh cấu trúc:
classDiagram
class Notification {
<<abstract>>
#INotificationChannel Channel
+SendAsync(to, raw)
}
class UrgentNotification
class InfoNotification
class MarketingNotification
Notification <|-- UrgentNotification
Notification <|-- InfoNotification
Notification <|-- MarketingNotification
class INotificationChannel {
<<interface>>
+DeliverAsync(to, subject, body, priority)
}
class EmailChannel
class SmsChannel
class SlackChannel
INotificationChannel <|.. EmailChannel
INotificationChannel <|.. SmsChannel
INotificationChannel <|.. SlackChannel
Notification o-- INotificationChannel : giữ
Hai hierarchy mọc trên đôi chân riêng.
Khi nào dùng Bridge thay vì chỉ composition đơn giản?
Bridge là một hình dạng cụ thể của composition. Quy tắc quyết định:
- Dùng Bridge khi hai trục là khái niệm hạng nhất trong domain (notification style, delivery channel) mà caller cần nói tới và cả hai dự kiến mọc.
- Dùng composition trơn khi một trục là dependency concrete duy
nhất — như mọi notification dùng cùng
IClockcho timestamp. Không có hierarchy thứ hai; inject dependency là đủ. - Dùng Strategy khi một class cần đổi hành vi nhưng cấu trúc không tách thành hierarchy.
Một case thất bại luôn lộ Bridge bị áp dụng quá đà: khi phía abstraction chỉ có một subclass concrete suốt nhiều tháng, việc tách là đầu cơ. Gập abstraction lại về cái có. Tách lại ngày style thứ hai thật sự tới.
So sánh Bridge với Adapter và Strategy thế nào?
Các pattern đứng cạnh nhau nhưng trả lời câu hỏi khác:
| Pattern | Câu hỏi nó trả lời | Số hierarchy | Hình hài .NET hiện đại |
|---|---|---|---|
| Bridge | Hai trục vuông góc mọc thế nào không bùng N×M class? | Hai hierarchy song song | Composition + DI |
| Adapter | Class lạ vừa interface tôi thế nào? | Một bọc cái kia | Class wrapper |
| Strategy | Một class đổi một thuật toán thế nào? | Không (một class, một interface) | IStrategy hoặc Func<> inject |
Quy tắc rõ ràng nhất: Bridge là tách; Adapter là khớp; Strategy là đổi. Nếu codebase đã có sự tách (hai hierarchy nên compose) và bạn đang thiết kế bây giờ, bạn đang làm Bridge. Nếu có sự tách nhưng chỉ cần cắm một mảnh trên một trục, đó là Strategy.
Một ví dụ thật trong C# / .NET 10 trông thế nào?
Hình dạng đầy đủ ship được production. Để ý DI registration: mỗi trục là concern riêng; consumer xin kết hợp họ muốn qua factory nhỏ hoặc theo tên.
public enum Priority { Low, Normal, High }
public interface INotificationChannel
{
Task DeliverAsync(string recipient, string subject, string body,
Priority priority, CancellationToken ct);
}
public sealed class EmailChannel : INotificationChannel
{
private readonly SmtpClient _smtp;
public EmailChannel(SmtpClient smtp) => _smtp = smtp;
public Task DeliverAsync(string to, string subject, string body, Priority p, CancellationToken ct)
=> _smtp.SendMailAsync(new MailMessage("noreply@shop.example", to, subject, body), ct);
}
public sealed class SmsChannel : INotificationChannel
{
private readonly ITwilioClient _twilio;
public SmsChannel(ITwilioClient twilio) => _twilio = twilio;
public Task DeliverAsync(string to, string subject, string body, Priority p, CancellationToken ct)
=> _twilio.SendMessageAsync(to, body, ct); // SMS bỏ qua subject
}
public abstract class Notification
{
protected INotificationChannel Channel { get; }
protected Notification(INotificationChannel channel) => Channel = channel;
public abstract Task SendAsync(string recipient, string raw, CancellationToken ct);
}
public sealed class UrgentNotification : Notification
{
public UrgentNotification(INotificationChannel ch) : base(ch) { }
public override Task SendAsync(string to, string raw, CancellationToken ct)
=> Channel.DeliverAsync(to, "🚨 URGENT", $"URGENT: {raw}", Priority.High, ct);
}
public sealed class InfoNotification : Notification
{
public InfoNotification(INotificationChannel ch) : base(ch) { }
public override Task SendAsync(string to, string raw, CancellationToken ct)
=> Channel.DeliverAsync(to, "Update", raw, Priority.Normal, ct);
}
// Composition root ------------------------------------------------
builder.Services.AddSingleton<SmtpClient>();
builder.Services.AddSingleton<ITwilioClient, TwilioClient>();
builder.Services.AddKeyedScoped<INotificationChannel, EmailChannel>("email");
builder.Services.AddKeyedScoped<INotificationChannel, SmsChannel>("sms");
// Kết hợp style + channel qua factory nhỏ ở biên
public sealed class NotificationFactory
{
private readonly IKeyedServiceProvider _sp;
public NotificationFactory(IKeyedServiceProvider sp) => _sp = sp;
public Notification Create(string style, string channelKey)
{
var channel = _sp.GetRequiredKeyedService<INotificationChannel>(channelKey);
return style switch
{
"urgent" => new UrgentNotification(channel),
"info" => new InfoNotification(channel),
_ => throw new ArgumentException($"Unknown style: {style}")
};
}
}
// Caller
var ship = factory.Create("info", "email");
await ship.SendAsync(buyer.Email, $"Order #{order.Id} shipped", ct);
var alert = factory.Create("urgent", "sms");
await alert.SendAsync(buyer.Phone, "Payment failed — please retry", ct);
Cái không xuất hiện: chín class kiểu EmailUrgent, SmsUrgent,
v.v. Ma trận sụp xuống thành hai hierarchy ba cái mỗi cái. Thêm
channel Push và style Onboarding quý sau — đó là hai file mới, không
phải tám.
Đọc tiếp gì trong series?
- Bài trước: Adapter — khi class có sẵn có interface sai và bạn không thiết kế lại được.
- Bài kế: Composite — khi cùng thao tác áp dụng cho item đơn lẻ và nhóm item.
- Tham chiếu chéo: Strategy — khi cần đổi một thuật toán, không phải tách hai hierarchy.
- Tham chiếu chéo: Factory Method
—
NotificationFactoryở trên chính là pattern đó, dùng kết hợp hai trục Bridge. - Cây quyết định: Cách chọn design pattern phù hợp.
Một thói quen tư duy ngăn cleanup Bridge tương lai: mỗi lần bạn sắp
viết new ThingForXForY (tên class có hai từ khoá), dừng lại. Có
Bridge đang chờ. Đa số lần code đúng là new XThing(yImplementation)
— composition của hai hierarchy, không phải class thứ ba cho tích
chéo.
Câu hỏi thường gặp
Bridge khác Adapter ở điểm gì?
Khi nào Bridge thừa thãi trong C# hiện đại?
Bridge khác Strategy pattern thế nào?
Dependency injection có thay được Bridge pattern hoàn toàn không?
EmailUrgentNotification thành UrgentNotification và EmailChannel. Khi đã tách, DI là cách tự nhiên để kết hợp lúc runtime — services.AddScoped<INotificationChannel, EmailChannel>() cộng services.AddTransient<UrgentNotification>() rồi constructor lo phần còn lại.