Feed Rss



Апр 22 2009

GoF. Strategy, Composite, Decorator

Мне довольно часто приходится общаться с разработчиками разного уровня мастерства, которые пишут на различных языках программирования, и практически все в определенный момент сталкиваются с проблемой понимания и применения шаблонов. Проблема при этом не в том, что они не знаеют как реализовать тот или иной паттерн, а в том — зачем и в какой ситуации это делать. Основываясь на своем опыте и тех книгах что я прочел, могу предположить что проблема связана с тем, что большинство авторов слищком зацикливаются на описании повседневных, довольно тривиальных и интуитивно понятных проблем. Да, я понимаю что такая «попса» зачастую найдет больше применения, но сферу применения шаблонов начинающими разработчиками это существенно сужает. Мне от чего-то кажется, что читая в каждой книге о применении шаблона Decorator, для дополнения поведения объекта Window, начинающий программист только для этого Window Decorator и станет применять — сложно представить какого рода задачи им еще решаются. И если с шаблоном Одиночка (Singleton) еще все понятно и дела обстоят не так плачевно, то применение шаблона Composite сводиться исключительно к деревьям в прямом смысле этого слова — представить структуры иного вида для этого шаблона у начинающих не хватает воображения или же опыта. А откуда этому опыту взяться, если им каждый раз говорят, что Composite используется для представления деревьев и точка. Сегодня я хочу показать применение паттернов Strategy, Composite и Decorator на живых примерах, которые к тому же не являются класическими примерами для книг о шаблонах.

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

Думаю у многих первой же мыслью стало захардкодить какой-от параметр в объекте пользователя с датой начала действия абонемента, а затем простым условием из if в местах предоставления ссылок для скачивания решать выдавать ли еще контент бесплатно или халява закончилась и ссылки прийдется покупать. Да — так проще всего, но насколько подобное решенеи поддается масштабированию? Не поддается. Во всяком случае, если проект вы собираетесь развивать дальше, а не забросить через пару недель.

Что ж, что б выбрать более гибкое решение, прийдется погрузиться в более детальный анализ проблемы и рассмотреть различные точки вариаций. Что нам известно наверняка?

  • товары могут продаваться без скидок (начальное условие разрабатываемой системы)
  • пользователь может каким-то образом получить скидку на товары
  • в связи с тем, что цены на товары у нас разные, а скачивать можно все и без ограничений — скидка является процентной (и ставка составляет 100%)
  • скидка может действовать на протяжении какого-то срока (6 месяцев)

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

  • скидка может быть абсолютной (например на 10у.е) — такой себе подарочный сертификат
  • скидка может применяться к конкретному товару, или группе товаров
  • в зависимости от типа пользователя (гость, зарегистрированый, постоянный покупатель) скидка может увеличиваться

Более того — если будет введено несколько типов скидок, то вероятность их взаимопересечения возрастает и нужно придумать что делать в таком случае. Например какая цена должна быть на товар, если на него действует 10% постоянная скидка на товар, 5% на все товары есть у покупателя как у постоянного и еще 10у.е. подарочный сертификат обнаружиться?

Ну что ж, давайте немного порисуем, а затем я постараюсь объяснить в порядке построения все принятые решения и связи между сущностями:

Item & PricingStrategyInterface

Итак, первым нашим классом станет Item. Он описывает предметы, продаваемые на нашем сайте. Для упрощения системы предположим что покупка совершается моментально и пользователь не набирает предварительно товары в корзину (иначе было бы необходимо ввести объект Продажа).

Класс товара обладает двумя необходимыми нам в примере методами getPrice() и getPreDiscountPrice(), первый должен возвращать нам цену на товар в зависимости от примененных скидок, а второй — базовую цену на товар. Тут стоит зметить, что наделять этот класс обязанностями возвращать цену на основании скидок было бы не правильно и приводит к уменьшению зацепления, но для примера сойдет. Итак, мы имеем начальную ситуацию, когда на товар в момент времени может действовать только один тип скидок. Пусть она будет процентной и составлять 10%. Так как подсчет разного типа скидок по своей сути является разным поведением одного и того же метода getPrice(), то самое время ввести шаблон Strategy.

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

PricingStrategy_Absolute & PricingStrategy_Percent

Имея интерфейс мы можем реализовать два класса с одинаковыми сигнатурами, но разным поведением: PricingStrategy_Absolute и PricingStrategy_Percent. Первый будет отвечать за скидки абсолютные (на 10у.е. к примеру), а второй — за относительный (процентные).

Полиморфные методы getPrice() для этих классов будут выглядеть примерно так:

return item.getPreDiscountPrice() - discount;

для PricingStrategy_Absolute, где discount — приватная переменная со значением скидки

return item.getPreDiscountPrice() * ( 1 - percent );

для PricingStrategy_Percent, где percent — приватная переменная со значением процента скидки

Теперь мы можем закончить реализацию метода getPrice() класса Item, добавив приватное свойство pricing_strategy — действующая на товар скидка:

return pricing_strategy.getPrice( this );

Таким образом, заменяя объект pricing_strategy у объекта товара мы можем менять поведение подсчета цены для разных видов скидок.

PricingStrategy_Composite_BestForCustomer

Но как быть, если на один товар может действовать несколько скидок? Какую окончательную цену выбрать? Для объединения скидок в виде атомарного объекта предлагаю воспользоваться шаблоном Composite.

Паттерн Компоновщик подразумевает под своим использованием возможность обращаться с группой объектов, как с одним единственным того же типа. Значит, если мы определим класс PricingStrategy_Composite_BestForCustomer, то он должен делать две вещи: реализовывать интерфейс обычной одинарной стратегии скидок, и уметь добавлять в себя другие объекты скидок.

Значит нам, кроме реализации PricingStrategyInterface, необходимо добавить хринилище скидок pricing_strategies (как приватное свойство) и метод add(), который это самое хранилище стратегиями и будет наполнять.

Ну а всю суть шаблона передает реализация полиморфного метода getPrice():

price = 0;
foreach ( picing_strategies as strategy ) {
    price = min( price, strategy.getPrice() );
}
return price;

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

PricingStrategy_Decorator & PricingStrategy_Decorator_Period

Еще один незатронутый вопрос — как расширить возможности скидок, например добавив им время действия. Один из вариантов — добавить еще один класс, реализуюший интерфейс PricingStrategyInterface, но тогда для абсолютной и процентной скидок, действующих в каком-то интервале времени, прийдется добавлять по дополнительному классу: PricingStrategy_Absolute_ForPeriod и PricingStrategy_Percent_ForPeriod. И чем больше у скидок будет появляться расширенных возможностей, тем больше классов прийдется описывать. А зачем плодить в геометрической прогресии сущности без надобности, если есть шаблон Decorator?

Шаблон декоратор позволяет расширять поведение классов, без использования наследования. Для этого в нашем примере мы реализуем интерфейс PricingStrategyInterface абстрактным классом PricingStrategy_Decorator, от которого будут наследоваться все необходимые нам расширения. Декоратор обычно принимает в конструктор объект, который будет декорировать и записывает его в приватное свойство. А в унаследованных классах мы можем реализовывать дополнительные поведения. Например так бы выглядел декоратор метода getPrice(), делающий скидки сезонными:

if ( date_start + period <= date_now() ) {
    return decorated_pricingstrategy.getPrice( item );
} else {
    return item.getPrice();
}

Принимая в конструкторе два дополнительных параметра date_start и period, унаследованый класс обеспечивает внутри класса getPrice() все необходимые преобразования поведения.

Примеры работы

Как же теперь этим всем воспользоваться? Попробую показать на примере из начала статьи. Решим задачу: «предостаавляя на сайте платный контет, необходимо реализовать возможность купить абонемент на бесплатное использование ресурса в течении 6 месяцев.»

Что нам для этого понадобиться? Первое: нужно сконфигурировать декоратором объект стратегии ценообразования:

ps = new PricingStrategy_Decorator_Period(
    new PricingStrategy_Absolute( 1 ), // 100% скидка
    date(), // сегодняшняя дата
    6 * MONTH // длительность действия скидки
);

И второе — для приобретаемого товара добавить стратегию получения цены:

item = new Item();
item.setDiscountStrategy( ps );

А дальше мы можем обращаться с объектом товара, даже не предполагая наличия каких-либо скидок, отвечать за необходимые преобразования будут ответственные за это классы:

price = item.getPrice();

В последнем участке кода стоит все же добавить, что конфигурировать товар стоит через Фабричный метод или Фабрику, но это уже материалы для новой заметки…

Понравился пост? Подпишись на RSS!

Метки: , , , , , , , ,

7 ответов на “GoF. Strategy, Composite, Decorator”

  1. bladeofsteel says:

    Спасибо, отличная статья!

    Правда немного сложновата для начинающих применять патерны. Но явно полезная для их понимания.

  2. eater says:

    Мне кажется что в реализации метода getPrice() у класов PricingStrategy_Absolute и PricingStrategy_Percent нужно вызывать метод item.getPreDiscountPrice(), иначе будет зацикливание.

  3. Алексей Токарь says:

    2<>eater<>: да, Вы правы, просто методы переименовал в процессе написания и забыл изменить имена в теле. правлю. спасибо

  4. rid says:

    В декораторе наверное тоже вместо
    item.getPrice();
    надо возвращать item.getPreDiscountPrice()

  5. Dattaya says:

    Если есть возможность, сделайте, пожалуйста, изображение http://alexeytokar2.files.wordpress.com/2009/04/gof_strategy_composite_decorator.png?w=300 доступным. У меня оно не отображается.

    • Алексей Токарь says:

      К сожалению, оно было утеряно :(

      • Dattaya says:

        Спасибо большое. Очень хорошая статья.