Type Structural
Purpose Decouple an abstraction from its implementation so that each may vary independently.

Description

The intent of the Bridge pattern is to decouple an abstraction from its implementation so that the two can vary independently. Rather than binding abstraction and implementation together through inheritance, the pattern uses composition to keep them separate. This reduces tight coupling, avoids an explosion of subclasses, and increases the overall flexibility of the design.

With Bridge, the abstraction maintains a reference to an implementor and delegates implementation-specific work to it. This allows both sides of the structure to evolve independently: abstractions can be extended without changing the implementation hierarchy, and implementations can be varied without affecting the abstraction hierarchy.

The abstraction in a Bridge can expose one operation or many. It may be a relatively simple interface or a richer one with multiple methods, properties, and related behaviour, provided the design continues to preserve high cohesion and low coupling. The essential point is that the abstraction remains separated from its concrete implementations.

Bridge is primarily concerned with separating an abstraction from its implementation so both can change independently.


Overview

Bridge
Abstraction The interface that the client sees.
Bridge (Implementor) The Implementor defines the interface used by the Abstraction and is implemented by the concrete implementation classes.
ConcreteImplementers Implementations of the Implementor interface.
Refined Abstractions Optionally descendants of the Abstraction class which can evolve independently of the Bridge ConcreteImplementor(s). Descendants of the Abstraction can further showcase the advantages of the Bridge pattern, but it's not a requirement for the pattern to be useful or applicable.

First Implementation

In this example, the Bill (Abstraction) uses an IDiscountPolicy to calculate the price of the dinner after applying a discount. IDiscountPolicy (Implementor) provides that interface, which is implemented by the various discount policy classes. The client passes the discount policy and price into the Bill via constructor injection.

// Bridge (Implementor)

public interface IDiscountPolicy
{
    decimal CalculateDiscountedPrice(decimal price);
}

// Concrete Implementors

public class NoDiscount : IDiscountPolicy
{
    public decimal CalculateDiscountedPrice(decimal price) 
        => price;
}

public class FixedDiscount(decimal amount) : IDiscountPolicy
{
    public decimal CalculateDiscountedPrice(decimal price) 
        => price - amount;
}

public class PercentageDiscount(decimal percentage) : IDiscountPolicy
{
    public decimal CalculateDiscountedPrice(decimal price) 
        => price - percentage * price / 100;
}

// Abstraction

public class Bill(int price, IDiscountPolicy policy)
{
    public decimal TotalDue => policy.CalculateDiscountedPrice(price);    
}

// Client

public static class BridgeDemo
{
    public static void Execute()
    {
        ProvideBill(47, 120, new NoDiscount());
        ProvideBill(21, 65, new PercentageDiscount(20));
        ProvideBill(38, 150, new FixedDiscount(25));
    }
    
    private static void ProvideBill(int tableId, int price, IDiscountPolicy policy)
    {
        var bill = new Bill(price, policy);
        Console.WriteLine($"Table {tableId} - Total Due: {bill.TotalDue:C}");
    }
}

// outputs:

// Table 47 - Total Due: $120.00
// Table 21 - Total Due: $52.00
// Table 38 - Total Due: $125.00

Second Implementation

In this second implementation, refined abstractions are introduced as well as a new discount concrete implementation to demonstrate evolution of the parts independently of each other.

In addition, Abstraction was made abstract to show that it can literally be an abstraction or a concrete class - the pattern terminology can be confusing at times.

// Bridge (Implementor)

public interface IDiscountPolicy
{
    decimal CalculateDiscountedPrice(decimal price);
}

// Concrete Implementors

public class NoDiscount : IDiscountPolicy
{
    public decimal CalculateDiscountedPrice(decimal price) 
        => price;
}

public class FixedDiscount(decimal amount) : IDiscountPolicy
{
    public decimal CalculateDiscountedPrice(decimal price) 
        => price - amount;
}

public class PercentageDiscount(decimal percentage) : IDiscountPolicy
{
    public decimal CalculateDiscountedPrice(decimal price) 
        => price - percentage * price / 100;
}

public class PensionerDiscount : IDiscountPolicy
{
    public decimal CalculateDiscountedPrice(decimal price) 
        => price - 50 * price / 100;
}

// Abstraction

public abstract class Bill(decimal price, IDiscountPolicy policy)
{
    public decimal Price { get; } = price;

    public IDiscountPolicy Policy { get; init; } = policy;
    
    public decimal TotalDue => Policy.CalculateDiscountedPrice(Price);    
}

// Refinements of Abstraction

public class TakeawayBill(decimal price, IDiscountPolicy policy) : Bill(price, policy);

public class LunchBuffetBill(IDiscountPolicy policy) : Bill(10, policy)
{
    public LunchBuffetBill() : this(new NoDiscount()){}
}

public class DinnerBuffetBill(IDiscountPolicy policy) : Bill(20, policy)
{
    public DinnerBuffetBill() : this(new NoDiscount()){}
}

// Client

public static class BridgeDemo
{
    public static void Execute()
    {
        Console.WriteLine("Takeaway Bills");
        
        Charge(new TakeawayBill(2.50M, new NoDiscount()));
        Charge(new TakeawayBill(12.80M, new PercentageDiscount(20)));
        
        Console.WriteLine("Lunch Time Buffet Bills:");
        
        ProvideBill<LunchBuffetBill>(21, new PercentageDiscount(20));
        ProvideBill<LunchBuffetBill>(38, new FixedDiscount(5));
        
        Console.WriteLine("Dinner Time Buffet Bills:");
        
        ProvideBill<DinnerBuffetBill>(14, new PercentageDiscount(25));
        ProvideBill<DinnerBuffetBill>(16, new FixedDiscount(7));
        ProvideBill<DinnerBuffetBill>(22, new PensionerDiscount());
    }

    private static void Charge(Bill bill)
        => Console.WriteLine($"\tThat's: {bill.TotalDue:C} thank you.");
    
    private static void ProvideBill<T>(int tableId, IDiscountPolicy policy) 
        where T:Bill, new()
    {
        var bill = new T { Policy = policy };
        Console.WriteLine($"\tTable {tableId} - Total Due: {bill.TotalDue:C}");
    }
}

// outputs:

// Takeaway Bills
//         That's: $2.50 thank you.
//         That's: $10.24 thank you.
// Lunch Time Buffet Bills:
//         Table 21 - Total Due: $8.00
//         Table 38 - Total Due: $5.00
// Dinner Time Buffet Bills:
//         Table 14 - Total Due: $15.00
//         Table 16 - Total Due: $13.00
//         Table 22 - Total Due: $10.00

Summary

The Bridge pattern is useful when both an abstraction and its implementation need to vary independently. By connecting the two through composition rather than binding them together through inheritance, new abstractions and implementations can be introduced without requiring a corresponding proliferation of subclasses.

For more information, please see here.