Cấu trúc Nâng cao 8 phút đọc

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
  1. Bridge pattern giải quyết bài toán gì trong C#?
  2. Bùng nổ class hiện ra trong code thật thế nào?
  3. Cấu trúc Bridge trong .NET 10 trông thế nào?
  4. Khi nào dùng Bridge thay vì chỉ composition đơn giản?
  5. So sánh Bridge với Adapter và Strategy thế nào?
  6. Một ví dụ thật trong C# / .NET 10 trông thế nào?
  7. Đọ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 notificationchannel 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ể:

  1. Style notification × channel. Như trên. Ba style, ba channel, dự kiến mọc cả hai trục.
  2. 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.
  3. 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 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:

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?

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ì?
Adapter bọc class có sẵn để vừa interface khác — giải mismatch giữa hai bên đã tồn tại. Bridge là quyết định thiết kế từ đầu: bạn chọn tách hai trục biến đổi thành hai hierarchy riêng. Adapter là sửa; Bridge là kế hoạch.
Khi nào Bridge thừa thãi trong C# hiện đại?
Khi chỉ một trục thật sự biến đổi. Nếu có ba notification style nhưng chỉ một channel, bạn không cần Bridge — một hierarchy class với channel inline là đủ. Bridge xứng đáng khi cả hai trục đã có ít nhất hai biến thể hôm nay và ít nhất một sẽ tiếp tục mọc.
Bridge khác Strategy pattern thế nào?
Strategy cắm một thuật toán vào chỗ trước đó hard-code; bản thân abstraction là một class. Bridge to hơn: nó tách cả một hierarchy abstraction khỏi cả một hierarchy implementation và để cả hai cùng mọc. Bridge thường chứa Strategy ở phía implementation.
Dependency injection có thay được Bridge pattern hoàn toàn không?
DI thay được bước nối nhưng không thay được quyết định thiết kế. Bạn vẫn phải chọn tách class EmailUrgentNotification thành UrgentNotificationEmailChannel. 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.