Мост (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);