Andrey on .NET | Структурные шаблоны: Мост (Bridge)

Структурные шаблоны: Мост (Bridge)

Название шаблона

Мост (Bridge).

Тип

Структурный шаблон проектирования (Structural).

Описание

Шаблон Мост позволяет разделить объект на абстракцию и реализацию так, чтобы они могли изменяться независимо друг от друга.

Данный шаблон используется если необходимо:

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

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

Если посмотреть внимательно, то видно, что результат разделения одного объекта практически идентичен результату применения Адаптера. Поэтому необходимо рассматривать применение шаблона Мост при проектировании или рефакторинге группы объектов, связанных наследованием. В этом случае, после разделения базового объекта, возможно:

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

Лучше понять принцип разделения обязанностей между частями Моста можно используя платформенно-зависимые классы:

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

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

Разумеется область применения Моста не ограничена аппаратно- и платформенно-зависимыми классами. В других случая можно Аналогично разделять функциональность других объектов.

Таким образом, можно выделить следующих участников шаблона:

  1. Абстракция (Abstraction) – определяет базовый интерфейс для работы с клиентом.
  2. Уточненная абстракция (Refined abstraction) – наследует абстракцию и вносит дополнительные свойства и методы.
  3. Реализация (Implementor) – определяет интерфейс реализаций.
  4. Конкретная реализация (Concrete implementor) – обеспечивает определенную функциональность.

Очень важным моментом в проектировании Моста является разработка двух интерфейсов: абстракции и её взаимодействия с реализацией. Чем меньше будет в них привязка к конкретной реализации, тем проще будет заменить её в дальнейшем. Например, использование для задания координаты класса Point из .NET усложнит последующий перенос на WinAPI.

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

Сравнение Моста и механизма наследованияТакже стоит отметить, что использование шаблона Мост уменьшает число создаваемых классов. В приведенном выше примере, для поддержки новой платформы достаточно добавить только один класс – новую реализацию отображения точки. А при использовании только наследования, необходимо будет разработать аналоги всех исходных классов. Разница хорошо видна на схеме.

Кроме того, в случае с наследованием есть проблема добавления новых методов в подклассы. Такие методы не будут доступны через родительский интерфейс. Значит придется создавать новый, чтобы можно было подставить, например, LineNET вместо LineAPI и не потерять доступ к этим методам. При применении шаблона Мост такой проблемы нет в принципе, т.к. класс Line всего один.

Схожие шаблоны и их отличия

Мост Разделяет объект на абстракцию и реализацию. Используется для иерархии объектов. Позволяет отдельно изменять (наследовать) абстракцию и реализацию, повышая гибкость системы. Содержит объект(реализацию), который предоставляет методы для заданной абстракций и ее уточнений (наследников).
Адаптер Изменяет интерфейс объекта не изменяя его функциональности. Может адаптировать несколько объектов к одному интерфейсу. Позволяет повторно использовать уже существующий код. Содержит или наследует адаптируемый объект.
Фасад Объединяет группу объектов под одним специализированным интерфейсом. Упрощает работу с группой объектов, вносит новый уровень абстракции. Содержит или ссылается на объекты, необходимые для реализации специализированного интерфейса.
Декоратор Расширяет возможности объекта, изменяет его поведение. Поддерживает интерфейс декорируемого объекта, но может добавлять новые методы и свойства. Дает возможность динамически менять функциональность объекта. Является альтернативой наследованию (в том числе множественному). Содержит декорируемый объект. Возможна цепочка объектов, вызываемых последовательно.
Прокси Прозрачно замещает объект и управляет доступом к нему. Не изменяет интерфейс или поведение. Упрощает и оптимизирует работу с объектом. Может добавлять свою функциональность, скрывая ее от клиента. Содержит объект или ссылку на него, может управлять существованием замещенного объекта.
Компоновщик Предоставляет единый интерфейс для взаимодействия с составными объектами и их частями. Упрощает работу клиента, позволяет легко добавлять новые варианты составных объектов и их частей. Включается в виде интерфейса в составные объекты и их части.
Приспособленец

Не ставит целью изменение интерфейса объекта. Но это может потребоваться для получения обратно данных из вынесенной части состояния.

Позволяет уменьшить число экземпляров объекта в приложении и тем самым сэкономить его ресурсы. Выносит контекстно-зависимую часть состояния объекта вовне, заменяя несколько его экземпляров одним.

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

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

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

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);

Комментарии (6) -

Евгений 17.07.2011 23:00:16

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

@ Евгений: Примитивы насколько избитый пример, что решил не брать его. Но он хорошо показывает уменьшение числа классов (куча калькуляторов сложнее воспринимается).

Николай 17.06.2015 6:58:55

Добрый день, Андрей!
Как мне кажется поведенческий паттерн Стратегия мягко говоря очень схож с паттерном Bridge. Также реализация выносится в отдельный класс. Если и есть какие то отличая, то мне кажется, что они больше в области "философии", просто разные названия "дополнительных" классов: в одном случае реализация в другом поведение. Я лично вообще не вижу различая! Не могли бы вы на них указать?

Говоря о шаблонах проектирования всегда правильнее говорить о решаемых задачах (целях, которые необходимо достигнуть), а не о конкретных реализациях. Поэтому да, различие всегда будет "в области философии".

Что касается конкретного сравнения, то Стратегия больше о смене алгоритма работы "на лету". Причем, как мне кажется, в этом случае классы менее связаны между собой архитектурно. Например, в приложении обработки видео есть интерфейс для фильтра (стратегия обработки), куда может быть подставлен практически любой фильтр, созданный даже уже после реализации самой программы.

Мост мне видится более продуманной изначально архитектурой разделения (тот же пример с аппаратно-зависимыми классами, где добились уменьшения числа созданных классов). У Моста возможны комбинации реализаций и абстракций (в случае Стратегии один исходный контекст использует данную ему стратегию). Он зачастую упоминается в контексте наследования (и реализации и абстракции) и группы классов, в отличии от Стратегии. Ну только что терминология у него несколько запутанная ("абстракция" так и норовит в область интерфейсов утянуть, хотя это по сути самый что не на есть обычный класс, даже не абстрактный)

Евгений 08.12.2015 17:42:00

Все они используют исходную [реализацию] для рисования и для того, чтобы перенести их на новую платформу достаточно заменить реализацию. Мне кажется там опечатка, должна быть абстракция

Опечатки нет, т.к. непосредственно рисует конкретная реализация. Заменяя ее мы адаптируем все классы для работы на другой платформе. Ни абстракция (точка), ни ее уточнения (линии, круги) при этом не изменяются.

Добавить комментарий