bridgeМост (Bridge) – структурный шаблон проектирования, который позволяет отделить абстракцию от реализации таким образом, чтобы и абстракцию, и реализацию можно было изменять независимо друг от друга.

Даже если мы отделим абстракцию от конкретных реализаций, то у нас все равно все наследуемые классы будут жестко привязаны к интерфейсу, определяемому в базовом абстрактном классе. Для преодоления жестких связей и служит паттерн Мост.

Когда использовать данный паттерн?

  • Когда надо избежать постоянной привязки абстракции к реализации
  • Когда наряду с реализацией надо изменять и абстракцию независимо друг от друга. То есть изменения в абстракции не должно привести к изменениям в реализации

Общая реализация паттерна состоит в объявлении классов абстракций и классов реализаций в отдельных параллельных иерархиях классов.

 

Связь агрегации между классами Abstraction и Implementor фактически и представляет некоторый мост между двумя параллельными иерархиями классов. Собственно поэтому паттерн получил название Мост.

Формальное описание паттерна на языке C#:

class Client
{
    static void Main()
    {
        Abstraction abstraction;
        abstraction = new RefinedAbstraction(new ConcreteImplementorA());
        abstraction.Operation();
        abstraction.Implementor=new ConcreteImplementorB();
        abstraction.Operation();
    }
}
abstract class Abstraction
{
    protected Implementor implementor;
    public Implementor Implementor
    {
        set { implementor = value; }
    }
    public Abstraction(Implementor imp)
    {
        implementor = imp;
    }
    public virtual void Operation()
    {
        implementor.OperationImp();
    }
}
 
abstract class Implementor
{
    public abstract void OperationImp();
}
 
class RefinedAbstraction : Abstraction
{
    public RefinedAbstraction(Implementor imp)
        : base(imp)
    {}
    public override void Operation()
    {
    }
}
 
class ConcreteImplementorA : Implementor
{
    public override void OperationImp()
    {
    }
}
 
class ConcreteImplementorB : Implementor
{
    public override void OperationImp()
    {
    }
}

Участники

  • Abstraction: определяет базовый интерфейс и хранит ссылку на объект Implementor. Выполнение операций в Abstraction делегируется методам объекта Implementor
  • RefinedAbstraction: уточненная абстракция, наследуется от Abstraction и расширяет унаследованный интерфейс
  • Implementor: определяет базовый интерфейс для конкретных реализаций. Как правило, Implementor определяет только примитивные операции. Более сложные операции, которые базируются на примитивных, определяются в Abstraction
  • ConcreteImplementorA и ConcreteImplementorB: конкретные реализации, которые унаследованы от Implementor
  • Client: использует объекты Abstraction

Теперь рассмотрим реальное применение. Существует множество программистов, но одни являются фрилансерами, кто-то работает в компании инженером, кто-то совмещает работу в компании и фриланс. Таким образом, вырисовывается иерархия различных классов программистов. Но эти программисты могут работать с различными языками и технологиями. И в зависимости от выбранного языка деятельность программиста будет отличаться. Для решения описания данной задачи в программе на C# используем паттерн Мост:

class Program
{
    static void Main(string[] args)
    {
        // создаем нового программиста, он работает с с++
        Programmer freelancer = new FreelanceProgrammer(new CPPLanguage());
        freelancer.DoWork();
        freelancer.EarnMoney();
        // пришел новый заказ, но теперь нужен c#
        freelancer.Language = new CSharpLanguage();
        freelancer.DoWork();
        freelancer.EarnMoney();
 
        Console.Read();
    }
}
 
interface ILanguage
{
    void Build();
    void Execute();
}
 
class CPPLanguage : ILanguage
{
    public void Build()
    {
        Console.WriteLine("С помощью компилятора C++ компилируем программу в бинарный код");
    }
 
    public void Execute()
    {
        Console.WriteLine("Запускаем исполняемый файл программы");
    }
}
 
class CSharpLanguage : ILanguage
{
    public void Build()
    {
        Console.WriteLine("С помощью компилятора Roslyn компилируем исходный код в файл exe");
    }
 
    public void Execute()
    {
        Console.WriteLine("JIT компилирует программу бинарный код");
        Console.WriteLine("CLR выполняет скомпилированный бинарный код");
    }
}
 
abstract class Programmer
{
    protected ILanguage language;
    public ILanguage Language
    {
        set { language = value; }
    }
    public Programmer (ILanguage lang)
    {
        language = lang;
    }
    public virtual void DoWork()
    {
        language.Build();
        language.Execute();
    }
    public abstract void EarnMoney();
}
 
class FreelanceProgrammer : Programmer
{
    public FreelanceProgrammer(ILanguage lang) : base(lang)
    {
    }
    public override void EarnMoney()
    {
        Console.WriteLine("Получаем оплату за выполненный заказ");
    }
}
class CorporateProgrammer : Programmer
{
    public CorporateProgrammer(ILanguage lang)
        : base(lang)
    {
    }
    public override void EarnMoney()
    {
        Console.WriteLine("Получаем в конце месяца зарплату");
    }
}

В роли Abstraction выступает класс Programmer, а в роли Implementor – интерфейс ILanguage, который представляет язык программирования. В методе DoWork() класса Programmer вызываются методы объекта ILanguage.

Языки CPPLanguage и CSharpLanguage определяют конкретные реализации, а классы FreelanceProgrammer и CorporateProgrammer представляют уточненные абстракции.

Таким образом, благодаря применению паттерна реализация отделяется от абстракции. Мы можем развивать независимо две параллельные иерархии. Устраняются зависимости между реализацией и абстракцией во время компиляции, и мы можем менять конкретную реализацию во время выполнения.

Реализация шаблона в общем виде

  • определяем интерфейс абстракции:
    • разрабатываем его исходя из задачи
    • или берем интерфейс готового класса;
  • определяем функции реализации и её интерфейс (может не совпадать с интерфейсом абстракции);
  • создаем реализацию, наследующую этот интерфейс;
  • разрабатываем абстракцию;
  • абстракция создает и содержит экземпляр реализации и использует его для обслуживания вызовов своих методов;
  • создаем уточнения абстракции и новые версии реализации исходя из задачи;
  • при создании абстракции она создает или получает нужную реализацию;
  • клиент работает с абстракциями для получения нужного результата.

Примеры реализации

1. Поддержка различных реализаций

Разработаем калькулятор для расчета цены заказа.

Для описания товара в корзине покупателя создадим класс ItemInCart. Он содержит код товара (Id), количество (Quantity) и стоимость (Price).

public class ItemInCart
{
    public uint Id { get; set; }
    public uint Quantity { get; set; }
    public Money Price { get; set; }
}

Разделим обязанности между частями шаблона. Интерфейс абстракции будет определяться потребностями клиента: добавлять позиции товаров и получить стоимость всего заказа. В реализацию вынесем основные функции калькулятора: запросы цены единицы товара и стоимости доставки.

public interface IPriceCalc
{
    void AddItem(uint itemId, uint itemQuantity);
    Money GetTotalPrice(Address shippingTo);
}
 
public interface IPriceCalcImpl
{
    Money GetItemPrice(uint itemId, uint itemQuantity);
    Money GetShippingPrice(IEnumerator cart, Address shippingTo);
}

Перейдем к коду реализации. Первый вариант будет для самовывоза, т.е. стоимость доставки равна 0.

public class PriceCalcBaseImpl : IPriceCalcImpl
{
    public virtual Money GetItemPrice(uint itemId, uint itemQuantity) { /* Skipped */ }
    public virtual Money GetShippingPrice(IEnumerator cart, Address shippingTo)
    {
        return new Money() {
            Value = 0,
            Currency = Money.CurrencyType.RUR
        };
    }
}

Теперь создадим уточнение в котором добавим расчет стоимости доставки компанией А. Позже можно создать аналогичные уточнения и для других транспортных компаний. Процесс получения цены товара не изменился, поэтому заменим только метод расчета цены доставки.

public class PriceCalcCompanyAImpl : PriceCalcBaseImpl
{
    public override Money GetShippingPrice(IEnumerator cart, Address shippingTo)
    {
        /* Skipped */
    }
}

Для упрощения примера, список вариантов доставки зададим перечислением. Для выбора нужной реализации используем Фабричный метод:

public enum DeliveryCompany { Self, CompanyA }
 
public static class PriceCalcImplFabric
{
    public static IPriceCalcImpl GetPriceCalcImpl(DeliveryCompany company)
    {
        switch (company) {
            case DeliveryCompany.CompanyA:
                return new PriceCalcCompanyAImpl();
 
            default:
                return new PriceCalcBaseImpl();
        }
    }
}

Переходим к воплощению абстракций. Базовый вариант будет просто вычислять цену без учета скидок. Обратите внимание – абстракция содержит ссылку на реализацию и использует Фабричный метод для получения его экземпляра:

public class PriceCalcBasic : IPriceCalc
{
    private IPriceCalcImpl _impl;
 
    private readonly Dictionary<uint, ItemInCart> _cart
        = new Dictionary<uint,ItemInCart>();
 
    public PriceCalcBasic(DeliveryCompany company)
    {
        this._impl = PriceCalcImplFabric.GetPriceCalcImpl(company);
    }
 
    public virtual void AddItem(uint itemId, uint itemQuantity)
    {
        ItemInCart item = new ItemInCart { Id = itemId, Quantity = itemQuantity };
        this._cart.Add(itemId, item);
    }
 
    public virtual Money GetTotalPrice(Address shippingTo)
    {
        Money sum = new Money();
        var itemsList = this._cart.Values;
 
        foreach (ItemInCart item in itemsList) {
            Money itemPrice = this._impl.GetItemPrice(item.Id, item.Quantity);
            item.Price = itemPrice * item.Quantity;
 
            sum.Add(item.Price);
        }
 
        Money shippingPrice = this._impl.GetShippingPrice(
            itemsList.GetEnumerator(), shippingTo);
 
        sum.Add(shippingPrice);
 
        return sum;           
    }
}

Создадим уточнение – расчет цены с учетом скидок для постоянных клиентов. Будем использовать наследование от базового интерфейса. Но стоит помнить, что в некоторых случаях будет выгоднее наследоваться от исходной абстракции.

public class PriceCalcDiscount : IPriceCalc
{
    private IPriceCalcImpl _impl;
 
    private readonly Dictionary<uint, ItemInCart> _cart
        = new Dictionary<uint,ItemInCart>();
 
    public PriceCalcDiscount(DeliveryCompany company)
    {
        this._impl = PriceCalcImplFabric.GetPriceCalcImpl(company);
    }
 
    public virtual void AddItem(uint itemId, uint itemQuantity)
    {
        ItemInCart item = new ItemInCart { Id = itemId, Quantity = itemQuantity };
        this._cart.Add(itemId, item);
    }
 
    public virtual Money GetTotalPrice(Address shippingTo)
    {
        Money sum = new Money();
        var itemsList = this._cart.Values;
 
        foreach (ItemInCart item in itemsList) {
            Money itemPrice = this._impl.GetItemPrice(item.Id, item.Quantity) * 0.7;
            if (item.Quantity > 10) { itemPrice = itemPrice * 0.95; }
 
            item.Price = itemPrice * item.Quantity;
            sum.Add(item.Price);
        }
 
        Money shippingPrice = this._impl.GetShippingPrice(
            itemsList.GetEnumerator(), shippingTo);
 
        sum.Add(shippingPrice);
 
        return sum;           
    }
}

Теперь можно использовать базовый или дисконтный калькулятор, который учитывает способ указанный способ доставки. Для примера, код может выглядеть вот так:

public static Money GetCartTotal(IPriceCalc calc)
{
    /* Skipped */
 
    foreach (Tuple<uint,uint> item in itemsList) {
        calc.AddItem(item.Item1, item.Item2);
    }
 
    return calc.GetTotalPrice(userData.shippingAddr);
}
 
public static void ExecuteDemo()
{
    IPriceCalc calc1 = new PriceCalcBasic(DeliveryCompany.Self);
    Money price1 = GetCartTotal(calc1);
    Console.WriteLine(price1);
     
    IPriceCalc calc2 = new PriceCalcDiscount(DeliveryCompany.CompanyA);
    Money price2 = GetCartTotal(calc2);
    Console.WriteLine(price2);
}

Клиент может использовать как базовый абстрактный интерфейс, так и экземпляры нужных абстракций. А вот реализации от него скрыты Мостом и Фабричным методом.

Кроме того, не стоит забывать, что шаблон не запрещает модифицировать интерфейс уточненных абстракций. Поэтому одно из уточнений может, например, включать новые методы, которые возвращают отдельно стоимость доставки и каждой позиции в заказе и т.д.

2. Изменение реализации в процессе работы приложения

В примере выше есть один существенный недостаток: необходимо создавать новый экземпляр каждый раз, когда пользователь меняет транспортную компанию. Исправим это. В интерфейс абстракции добавим новый метод для выбора перевозчика:

public interface IPriceCalc
{
    void AddItem(uint itemId, uint itemQuantity);
    Money GetTotalPrice(Address shippingTo);
    void SetDeliveryCompany(DeliveryCompany company);
}

Сам метод появится в каждой уточненной абстракции:

public virtual void SetDeliveryCompany(DeliveryCompany company)
{
    this._impl = PriceCalcImplFabric.GetPriceCalcImpl(company);
}

Цель достигнута очень просто – заменой “на лету” старой реализации на новую, которая соответствует выбранной транспортной компании. В результате повторное порождение экземпляра абстракции не требуется:

IPriceCalc calc = new PriceCalcBasic(DeliveryCompany.Self);
calc1.AddItem(itemId, itemQuantity);
Money price3 = GetCartTotal(calc1);
 
calc1.SetDeliveryCompany(DeliveryCompany.CompanyA);
Money price4 = calc1.GetTotalPrice(userData.shippingAddr);