objects10 36

Шаблоны

Большинство задач, с которыми часто приходится сталкиваться программистам, уже давным-давно решены другими членами нашего сообщества. Шаблоны проектирования как раз и являются тем средством, с помощью которого люди могут поделиться друг с другом накопленным опытом. Как только шаблон становится всеобщим достоянием, он обогащает наш язык и позволяет легко поделиться с другими новыми идеями проектирования и их результатами. С помощью шаблонов проектирования просто выделяют общие задачи, определяют проверенные решения и описывают вероятные результаты.

Что такое шаблоны проектирования

"В мире программного обеспечения, шаблон — это реальное проявление генетической памяти организации."

— Гради Буч (Grady Booch), из книги Core J2EE Patterns

"Шаблон — это решение задачи в некотором контексте."

— "Банда четырех" (The Gang of Four), из книги Design Patterns: Elements of Reusable Object-Oriented Software

Как следует из приведенных выше цитат, шаблон проектирования — это задача, взятая из практики передовых программистов, решение которой проанализировано и объяснено. Задачи имеют свойство повторяться, и веб-программистам приходится решать их снова и снова. Как обработать входящий запрос? Как преобразовать данные в команды для нашей системы? Как ввести данные? Как представить результаты? Со временем мы находим более или менее изящные ответы на эти вопросы и создаем неформальный набор методов, которые затем снова и снова используем в своих проектах. Эти методы — и есть шаблоны проектирования.

С помощью шаблонов проектирования описываются и формализуются типовые задачи и их решения. В результате опыт, который нарабатывается с большим трудом, становится доступным широкому сообществу программистов. Шаблоны должны быть построены, главным образом, по "восходящему", а не "нисходящему" принципу. Их корень — в практике, а не в теории. Но это совсем не означает, что в шаблонах проектирования отсутствует элемент теории. Шаблоны основаны на реальных методах, используемых реальными программистами. Знаменитый приверженец шаблонов Мартин Фаулер говорит, что он открывает шаблоны, а не создает их. Поэтому многие шаблоны будут вызывать у вас чувство "дежавю" — ведь вы будете узнавать методы, которые используете сами.

Каталог шаблонов — это не книга кулинарных рецептов. Рецептам можно следовать буквально, а код можно скопировать и вставить в проект с незначительными изменениями. Вам не всегда нужно даже понимать весь код, используемый в этом "рецепте". Шаблоны проектирования описывают подходы к решению конкретных задач. Детали реализации могут существенно меняться в зависимости от более широкого контекста. От этого контекста зависит выбор используемого языка программирования, природа приложения, размер проекта и специфика задачи.

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

Сначала синтаксический анализатор сканирует текст на предмет поиска триггерных лексем (trigger tokens). Когда он находит соответствие, то передает лексему другому объекту-анализатору, который специализируется на чтении содержимого, расположенного внутри тегов. В результате данные шаблона продолжаются анализироваться до тех пор, пока не произойдет синтаксическая ошибка, не будет достигнут их конец, либо не будет найдена другая триггерная лексема. В случае нахождения такой лексемы, объект-анализатор также должен передать ее на обработку соответствующей программе — скорее всего, анализатору аргументов. Все вместе эти компоненты образуют то, что называется рекурсивным нисходящим синтаксическим анализатором.

Итак, вот наши участники: MainParser, TagParser и ArgumentParser. Мы также определили класс ParserFactory, который создает и возвращает эти объекты. Но, конечно, все идет не так гладко, как хотелось бы, и позже, на одном из совещаний, вы узнаете, что в шаблонах нужно поддерживать несколько синтаксисов. И теперь вам нужно создать параллельный набор объектов-анализаторов в соответствии с синтаксисом: OtherTagParser, OtherArgumentParser и т.д.

Вам поставлена такая задача: нужно генерировать различные наборы объектов в зависимости от конкретной ситуации, и эти наборы объектов должны быть более или менее "прозрачными" для других компонентов системы. Случилось так, что "Банда четырех" в своей книге определила следующую задачу для шаблона Abstract Factory (Абстрактная фабрика): "Предусмотреть интерфейс для создания семейств связанных или зависимых объектов без указания их конкретных классов". Этот шаблон нам прекрасно подходит! По сути нашей задачи мы как раз должны определить и очертить рамки использования данного шаблона.

Наименование шаблона уже само по себе очень ценно; таким образом создается что-то вроде общего словаря, который годами накапливается и используется в среде профессионалов. Такие условные обозначения помогают в совместных разработках, когда оцениваются и тестируются альтернативные подходы и их различные результаты. Например, при обсуждении семейств альтернативных синтаксических анализаторов вы можете просто сказать коллегам, что система создает каждый набор с помощью шаблона Abstract Factory. Они покивают с умным видом; кто-то сразу поймет, о чем идет речь, а кто-то отметит про себя, что нужно будет узнать об этом позже. Но суть в том, что у этого набора идей и различных результатов есть абстрактный описатель, который способствует созданию более кратких обозначений. Я проиллюстрирую это чуть позже.

И наконец, согласно международному законодательству, некорректно писать о шаблонах, не процитировав Кристофера Александера (Christopher Alexander), профессора архитектуры, работы которого оказали огромное влияние на первых сторонников объектно-ориентированных шаблонов. Вот что он пишет в книге A Pattern Language (Oxford University Press, 1977):

"Каждый шаблон описывает задачу, которая возникает снова и снова, а затем описывает суть решения данной задачи, так что вы можете использовать это решение миллион раз, каждый раз делая это по-разному."

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

Обзор шаблонов проектирования

По сути, шаблон проектирования состоит из четырех частей: имени, формулировки задачи, описания решения и результатов.

Имя

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

Формулировка задачи

Независимо от изящества решений (а некоторые из них действительно очень изящны), формулировка задачи и ее контекста — это основа шаблона. Сформулировать задачу гораздо труднее, чем применить какое-либо решение из каталога шаблонов. Это одна из причин того, что некоторые шаблоны могут применяться неправильно.

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

Решение

Решение сначала кратко описывается вместе с задачей. Оно также описывается подробно, как правило, с использованием UML-класса и диаграмм взаимодействия. В шаблон обычно включается пример кода.

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

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

Мартин Фаулер называет решения, описанные в шаблонах, "полусырыми", т.е. программист должен взять идею решения и закончить ее самостоятельно.

Результаты

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

Зачем используются шаблоны проектирования

Так в чем преимущества шаблонов? Учитывая, что шаблон — это поставленная задача и описанное решение, ответ, казалось бы, очевиден. Шаблоны помогают решать распространенные задачи. Но, конечно, шаблон — это нечто большее.

Сколько раз вы доходили до какого-то этапа в проекте и обнаруживали, что дальше идти некуда? Вполне вероятно, вам нужно вернуться немного назад, прежде чем начать снова. Определяя распространенные задачи, шаблоны помогают улучшить проект. И иногда первый шаг к решению — это осознание того, что есть проблема.

Определив и осознав проблему (и убедившись, что это именно та проблема), с помощью шаблона вы получаете доступ к решению, а также к анализу результатов его использования. Хотя шаблон не избавляет вас от необходимости рассмотреть последствия выбранного решения, вы, по крайней мере, будете уверены, что используете проверенный метод.

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

PHP и шаблоны проектирования

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

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

Например, для реализации шаблона Front Controller часто требуется довольно много времени. Хорошо, если инициализация имеет место один раз при запуске приложения, но гораздо хуже, если она происходит при каждом запросе. Это не значит, что нельзя использовать данный шаблон; я применял его в своей практике с очень хорошими результатами. Просто при обсуждении шаблона мы должны обязательно принять во внимание вопросы, связанные с PHP.

В PHP можно программировать, вообще не определяя никаких классов (хотя, учитывая продолжающееся развитие PEAR, вероятно, вы будете в некоторой степени оперировать объектами). Хотя здесь мы почти полностью концентрируемся на объектно-ориентированных решениях задач программирования, не считайте это залпом из всех орудий в войне между приверженцами разных стилей программирования. Шаблоны могут сосуществовать с другими, более традиционными, подходами. Убедительное доказательство тому — PEAR. В сборках PEAR очень изящно используются шаблоны проектирования. Они склонны быть объектно-ориентированными по своей природе. И это делает их более, а не менее, полезными в процедурных проектах. Поскольку сборки PEAR автономны и их сложность скрыта за четко описанным интерфейсом, их легко включить в проект любого типа.

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

Композиция и наследование

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

Проблема

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

На рисунке ниже приведен простой пример с использованием UML:

Родительский класс и два дочерних

Абстрактный класс Lesson моделирует занятие в колледже. Он определяет абстрактные методы cost() и chargeType(). На диаграмме показаны два реализующих класса, FixedPriceLesson и TimedPriceLesson, которые обеспечивают разные механизмы оплаты за занятия. С помощью этой схемы наследования я могу легко изменить реализацию занятия. Клиентский код будет знать только, что он имеет дело с объектом типа Lesson, поэтому детали механизма оплаты будут прозрачными.

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

На рисунке ниже показано решение проблемы "в лоб":

Неудачная структура наследования

Здесь показана явно неудачная иерархия. Мы не сможем в дальнейшем использовать это дерево наследования, чтобы управлять механизмами оплаты, не дублируя большие блоки функциональности. Эти стратегии оплаты повторяются в семействах классов Lecture и Seminar.

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

Код PHP
abstract class Lesson {
    protected $duration;
    const     FIXED = 1;
    const     TIMED = 2;
    private   $costtype;

    function __construct( $duration, $costtype=1 ) {
        $this->duration = $duration;
        $this->costtype = $costtype;
    }

    function cost() {
        switch ( $this->costtype ) {
            CASE self::TIMED :
                return (5 * $this->duration);
                break;
            CASE self::FIXED :
                return 30;
                break;
            default:
                $this->costtype = self::FIXED;
                return 30;
        }
    }

    function chargeType() {
        switch ( $this->costtype ) {
            CASE self::TIMED :
                return "Почасовая оплата";
                break;
            CASE self::FIXED :
                return "Фиксированная ставка";
                break;
            default:
                $this->costtype = self::FIXED;
                return "Фиксированная ставка";
        }
    }

    // Другие методы класса Lesson...
}

class Lecture extends Lesson {
    // Реализация класса Lecture ...
}

class Seminar extends Lesson {
    // Реализация класса Seminar ...
}

А вот как я должен работать с этими классами:

Код PHP
$lecture = new Lecture( 5, Lesson::FIXED );
print "{$lecture->cost()} ({$lecture->chargeType()})<br>"; 

$seminar= new Seminar( 3, Lesson::TIMED );
print "{$seminar->cost()} ({$seminar->chargeType()})<br>"; 

Диаграмма нового класса показана на рисунке ниже:

Иерархия наследования улучшена в результате удаления расчетов стоимости занятий из подклассов

Я сделал структуру класса намного более управляемой, но дорогой ценой. Использование условных операторов в данном коде — это шаг назад. Обычно мы стараемся заменить условный оператор полиморфизмом. А здесь я сделал противоположное. Как видите, это вынудило меня продублировать условный оператор в методах chargeType() и cost(). Похоже, я обречен на дублирование кода.

Использование композиции

Для решения данной проблемы я могу воспользоваться шаблоном Strategy. Этот шаблон используется для перемещения набора алгоритмов в отдельный тип. Перемещая код для вычисления стоимости, я могу упростить тип Lesson:

Перемещение алгоритмов в отдельный тип

Я создал еще один абстрактный класс CostStrategy, в котором определены абстрактные методы cost() и chargeType(). Методу cost() нужно передать экземпляр класса Lesson, который он будет использовать для расчета стоимости занятия. Мы обеспечиваем две реализации класса CostStrategy. Объекты Lesson работают только с типом CostStrategy, а не с конкретной реализацией, поэтому мы в любое время можем добавить новые алгоритмы расчета стоимости, создавая подклассы на основе CostStrategy. При этом не понадобится вносить вообще никаких изменений в классы Lesson.

Приведем упрощенную версию нового класса Lesson:

Код PHP
abstract class Lesson {
    private   $duration;
    private   $costStrategy;

    function __construct( $duration, CostStrategy $strategy ) {
        $this->duration = $duration;
        $this->costStrategy = $strategy;
    }

    function cost() {
        return $this->costStrategy->cost( $this );
    }

    function chargeType() {
        return $this->costStrategy->chargeType( );
    }

    function getDuration() {
        return $this->duration;
    }

    // Другие методы класса Lesson...
}

class Lecture extends Lesson {
    // Реализация класса Lecture ...
}

class Seminar extends Lesson {
    // Реализация класса Seminar ...
}

Конструктору класса Lesson передается объект типа CostStrategy, который он сохраняет в виде свойства. Метод Lesson::cost() просто вызывает CostStrategy::cost(). Точно так же Lesson::chargeType() вызывает CostStrategy::chargeType(). Такой явный вызов метода другого объекта для выполнения запроса называется делегированием. В нашем примере объект типа CostStrategy — делегат класса Lesson. Класс Lesson снимает с себя ответственность за расчет стоимости занятия и возлагает эту задачу на реализацию класса CostStrategy. Вот как осуществляется делегирование:

Код PHP
    function cost() {
        return $this->costStrategy->cost( $this );
    }

Ниже приведено определение класса CostStrategy вместе с реализующими его дочерними классами:

Код PHP
abstract class CostStrategy {
    abstract function cost( Lesson $lesson );
    abstract function chargeType();
}

class TimedCostStrategy extends CostStrategy {
    function cost( Lesson $lesson ) {
        return ( $lesson->getDuration() * 5 );
    }
    function chargeType() {
        return "Почасовая оплата";
    }
}

class FixedCostStrategy extends CostStrategy {
    function cost( Lesson $lesson ) {
        return 30;
    }

    function chargeType() {
        return "Фиксированная ставка";
    }
}

Во время выполнения программы я легко могу изменить способ расчета стоимости занятий, выполняемый любым объектом типа Lesson, передав ему другой объект типа CostStrategy. Этот подход способствует созданию очень гибкого кода. Вместо того чтобы статично встраивать функциональность в структуры кода, я могу комбинировать объекты и менять их сочетания динамически:

Код PHP
$lessons[] = new Seminar( 4, new TimedCostStrategy() );
$lessons[] = new Lecture( 4, new FixedCostStrategy() );

foreach ( $lessons as $lesson ) {
    print "Оплата за занятие <b>{$lesson->cost()} \$</b>. ";
    print "Тип оплаты: {$lesson->chargeType()}<br>";
}

В результате на выходе получим следующее:

Использование композиции при проектировании классов

Как видите, одно из следствий принятия этой структуры состоит в том, что мы рассредоточили обязанности наших классов. Объекты CostStrategy ответственны только за расчет стоимости занятия, а объекты Lesson управляют данными занятия.

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

Разделение

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

Проблема

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

На второй диаграмме в этой выше мы видели пример тесной связи. Поскольку логика схем оплаты повторяется в типах Lecture и Seminar, изменения в компоненте TimedPriceLecture приведут к необходимости внесения параллельных изменений в ту же логику в компоненте TimedPriceSeminar. Обновляя один класс и не обновляя другой, я нарушаю работу системы. При этом от интерпретатора PHP я не получу никакого предупреждения. Мое первое решение, с использованием условного оператора, породило аналогичную зависимость между методами cost() и chargeType().

Применяя шаблон Strategy, я преобразовал алгоритмы оплаты в тип CostStrategy, разместил их за общим интерфейсом и реализовал каждый из них только один раз.

Тесная связь другого рода может иметь место, когда много классов в системе внедрены явным образом в платформу или среду. Предположим, вы создаете систему, которая, например, работает с базой данных MySQL. Для запросов к серверу базы данных вы можете использовать такие функции, как mysqli_connect() и mysqli_query().

Но если вам понадобится развернуть систему на сервере, который не поддерживает MySQL, вы должны будете внести изменения в весь проект, чтобы использовать, например, SQLite. При этом вы будете вынуждены вносить изменения по всему коду, и вас ожидает перспектива поддерживать две параллельные версии приложения.

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

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

В PEAR эта проблема решена с помощью сборки PEAR::MDB2 (который пришел на смену сборке PEAR::DB). Это обеспечивает одну точку доступа для разных систем баз данных. А недавно, благодаря встроенному расширению PDO, эта модель была включена в сам язык PHP.

В классе MDB2 существует статический метод connect(), которому передается строка, содержащая имя источника данных (DSN). В зависимости от состава этой строки, метод возвращает экземпляр объекта, реализующего класс MDB2_Driver_Common. Поэтому для строки "mysql://" метод connect() возвращает объект типа MDB2_Driver_mysql, в то время как для строки, которая начинается с "sqlite://", он возвращает объект типа MDB2_Driver_sqlite. Структура этих классов показана на рисунке:

В сборке PEAR::MDB2 клиентский код отделен от объектов базы данных

Кроме того, сборка PEAR::MDB2 позволяет отделить код приложения от специфики платформы базы данных. При условии, что вы используете совместимый SQL-код, ваше приложение будет работать со многими СУБД, включая MySQL, SQLite, MSSQL и др. При этом вам не нужно будет вносить изменения в свой код, кроме, конечно, настройки DSN. Пожалуй, это единственное место в программе, где необходимо сконфигурировать контекст базы данных. На самом деле сборка PEAR::MDB2 до некоторой степени помогает также работать и с различными "диалектами" SQL — и это одна из причин, по которой вы можете решить использовать его, несмотря на скорость и удобство PDO.

Структура, показанная на рисунке выше имеет некоторое сходство с шаблоном Abstract Factory. Более простой по своей природе, он, тем не менее, имеет такое же назначение: генерировать объект, который реализует абстрактный интерфейс и не требует от клиента непосредственного создания экземпляра объекта.

Несмотря на то что в сборке MDB2 или в расширении PDO клиентский код отделен от специфики реализации платформы СУБД, свою часть работы вы все равно должны сделать. Если ваш (теперь уже универсальный) SQL-код рассредоточен по всему проекту, вы вскоре обнаружите, что единственное изменение какого-либо аспекта проекта может повлечь за собой каскад изменений во многих местах кода. И здесь самым типичным примером было бы изменение структуры базы данных, при котором добавление дополнительного столбца в таблице может повлечь за собой изменение SQL-кода многих повторяющихся запросов к базе данных. Поэтому вам следует подумать о том, чтобы извлечь этот код и поместить его в одну сборку, тем самым отделив логику приложения от специфики реляционной базы данных.

Ослабление связи

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

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

Если вы в коде будете использовать явные ссылки на класс Mailer или Texter, то тогда ваша система становится наглухо привязана к конкретному типу рассылки уведомлений. Это примерно то же самое, как привязаться к конкретному типу СУБД, используя в коде вызовы ее специализированных функций API.

Ниже приведен фрагмент кода, в котором детали реализации конкретной системы уведомления скрыты от кода, который ее использует:

Код PHP
class RegistrationMgr {
    function register( Lesson $lesson ) {
        // Что-то делаем с объектом типа Lesson

        // Рассылаем уведомления
        $notifier = Notifier::getNotifier();
        $notifier->inform( "Новое занятие: стоимость -  ({$lesson->cost()}\$)" );
    }
}

abstract class Notifier {
    
    static function getNotifier() {
        // Создадим конкретный класс согласно 
		// файлу конфигурации системы или другой логики

        if ( rand(1,2) == 1 ) {
            return new MailNotifier();
        } else {
            return new TextNotifier();
        }
    }

    abstract function inform( $message );
}

class MailNotifier extends Notifier {
    function inform( $message ) {
        print "Уведомление no e-mail: {$message}<br>";
    }
}

class TextNotifier extends Notifier {
    function inform( $message ) {
        print "Текстовoe уведомление: {$message}<br>";
    }
} 

Здесь я создал класс RegistrationMgr, который является простым клиентским классом для классов типа Notifier. Несмотря на то что класс Notifier абстрактный, в нем реализован статический метод getNotifier(), который создает и возвращает конкретный объект типа Notifier (TextNotifier или MailNotifier) в зависимости от сложившихся условий. В реальном проекте выбор конкретного класса типа Notifier должен определяться каким-нибудь гибким способом, например параметром в файле конфигурации. Здесь же я немного сжульничал и выбрал тип объекта случайным образом. Метод inform() классов MailNotifier и TextNotifier практически ничего не делает. Он просто выводит информацию, полученную в качестве параметра, а также дополняет ее типом сообщения, чтобы было видно, к какому классу он относится.

Обратите внимание на то, что только в методе Notifier::getNotifier() сосредоточена информация о том, какой конкретно объект типа Notifier должен быть создан. В результате я могу посылать уведомления из сотен разных участков программы, а при изменении типа уведомлений мне потребуется внести изменения только в один метод в классе Notifier.

Ниже приведен пример кода, вызывающий метод register() класса RegistrationMgr:

Код PHP
$lessons1 = new Seminar( 4, new TimedCostStrategy() );
$lessons2 = new Lecture( 4, new FixedCostStrategy() );

$mgr = new RegistrationMgr();
$mgr->register( $lessons1 );
$mgr->register( $lessons2 );

Вот что будет выведено в результате:

Пример использования ослабления связи между классами

На рисунке ниже приведена диаграмма классов нашего проекта:

В классе Notifier клиентский код отделен от кода реализации различных уведомителей

Обратите внимание на то, что эта структура очень сильно напоминает ту, которая используется в сборке PEAR::MDB2.

Проблемы применения шаблонов

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

Методология экстремального программирования (extreme Programming, XP) предлагает ряд принципов, которые применимы в данной ситуации. Первый принцип: "Вам необязательно это нужно" (обычно для него используют аббревиатуру YAGNI от You aren't going to need it). Как правило, данный принцип применяется для функций приложения, но он имеет смысл и для шаблонов.

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

Но если меня попросят создать простую форму для обратной связи небольшого веб-сайта, я могу запросто использовать процедурный код, поместив его на одну страницу с HTML-кодом. В этом случае мне не нужна гибкость в огромных масштабах, потому что я не буду что-либо строить на этой первоначальной основе. Мне не нужно использовать шаблоны, которые решают соответствующие задачи в более широких системах. Поэтому я применяю второй принцип экстремального программирования: "Сделайте самый простой работающий вариант". Работая с каталогом шаблонов, думайте о структуре и процессе решения, обращая внимание на пример кода. Но прежде чем применить шаблон, подумайте о том, "когда его использовать", найдите соответствующий раздел и прочитайте о результатах применения шаблона. В некоторых контекстах лечение может оказаться хуже болезни.

Диаграммы UML

Комментарии (0)

Результаты поиска по запросу

Система Orphus