Статьи

Шаблонный метод шаблона — переопределить логику стратегии

Первый раз, когда я посмотрел на шаблон Стратегии, это была любовь с первого взгляда. Логика паттерна, казалось, стала мифической панацеей, где использование Composition over Inheritance не только использовалось в полной мере, но и было настолько чистым и элегантным, что его очаровательному влиянию было трудно устоять. Что может быть более соблазнительным, чем передача нескольких исполнителей данного контракта одному или нескольким потребителям, позволяя им переваривать различные алгоритмы во время выполнения через узды полиморфизма?

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

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

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

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

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

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

Создание слайдера изображений на основе jQuery

Несмотря на то, что методы шаблонов широко представлены в нескольких средах, которые обеспечивают частичные реализации для данного компонента, позволяя подтипам переопределять абстрактные методы в соответствии с более изощренными и конкретными потребностями, в Интернете довольно трудно найти примеры, демонстрирующие, как использовать функциональность шаблона. в реальных случаях использования. Я ни в коем случае не собираюсь заполнять этот пробел, так как он может быть заполнен кем-то другим. Моя цель здесь — просто показать, как использовать шаблон при создании сегментов разметки, которые должны цепляться за данную последовательность. Хорошим примером этого является рендеринг слайдера изображений на основе jQuery, чей «алгоритм базовой стратегии» можно, с некоторыми тонкими наклонами, конечно, разделить на следующие шаги:

  • Визуализируйте целевые изображения внутри элемента уровня блока, подключенного к атрибуту «id» или «class».
  • Включить JQuery.
  • Включите плагин jQuery слайдера.
  • Рендеринг JavaScript, который запускает слайдер.

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

Вот как будет выглядеть вышеупомянутый базовый класс:

<?php namespace LibraryViewHelper; abstract class AbstractCycleSlider { protected $images = array(); public function __construct(array $images) { if (empty($images)) { throw new InvalidArgumentException( "No images were supplied."); } foreach ($images as $image) { $extension = pathinfo($image, PATHINFO_EXTENSION); if (!in_array($extension, array("gif", "jpg", "png"))) { throw new OutOfRangeException( "Only GIF/JPG/PNG files are allowed."); } } $this->images = $images; } // Hook method for rendering the target images protected function renderImages() { $output = '<div id="slider">'; foreach ($this->images as $image) { $output .= '<img src="' . $image . '">'; } $output .= "</div>"; return $output; } // Hook method for including jQuery and the Cycle plug-in protected function renderLibraries() { return <<<ENDHTML <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> <script src="http://cloud.github.com/downloads/malsup/cycle/jquery.cycle.all.latest.js></script> ENDHTML; } // Hook method for rendering a transition effect (overridden by // subclasses) abstract protected function renderEffect(); // Template method for rendering the slider public final function render() { return $this->renderImages() . $this->renderLibraries() . $this->renderEffect(); } } 

Единственная обязанность класса AbstractCycleSlider — обеспечить реализацию некоторых шагов, необходимых для рендеринга ползунка на основе элементарного цикла. Вся последовательность рендеринга в этом случае определяется методом render() который фактически действует как фактический «шаблон» для рассматриваемых шагов.

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

Если вы отсканируете через AbstractCycleSlider , вы увидите, где именно renderEffect() метод renderEffect() который должен быть реализован по линии одним или несколькими подклассами, таким образом генерируя несопоставимые функциональные версии базового слайдера.

Изысканные реализации базового слайдера

Создание нескольких работающих ползунков, управляемых циклом, является довольно простым процессом, который сводится к созданию подкласса базового AbstractCycleSlider и затем предоставляет конкретные реализации для его renderEffect() .

Следующие подклассы делают именно это:

 <?php namespace LibraryViewHelper; class FadeSlider extends AbstractCycleSlider { protected function renderEffect() { return <<<ENDHTML <script> jQuery(document).ready(function () { jQuery("#slider").cycle({fx: "fade"}); }); </script> ENDHTML; } } 
 <?php namespace LibraryViewHelper; class ScrollSlider extends AbstractCycleSlider { protected function renderEffect() { return <<<ENDHTML <script> jQuery(document).ready(function () { jQuery("#slider").cycle({fx: "scrollDown"}); }); </script> ENDHTML; } } 

Реализация метода renderEffect() предоставленного вышеприведенными подклассами, представляет некоторые фрагменты JavaScript, предназначенные для генерации пары эффектов перехода (Fade и Scroll), связанных с плагином Cycle. Это показывает, как делегировать реализацию логики стратегии, определенной в методе шаблона, нескольким подтипам.

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

 <?php use LibraryLoaderAutoloader, LibraryViewHelperFadeSlider, LibraryViewHelperScrollSlider; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader(); $autoloader->register(); $images = array( "sample_image1.jpg", "sample_image2.jpg", "sample_image3.jpg", "sample_image4.jpg", "sample_image5.jpg" }; $slider = new FadeSlider($images); echo $slider->render(); 

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

Если вы передумали и хотите вместо этого установить модный ползунок прокрутки, просто создайте экземпляр класса ScrollSlider вместо его аналога FadeSlider . Использование существующих ползунков или создание нескольких новых сводится к предоставлению пользовательских реализаций для renderEffect() .

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

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

Работа с разными плагинами

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

 <?php namespace LibraryViewHelper; abstract class AbstractSlider { protected $images = array(); public function __construct(array $images) { // the same constructor implementation } // Hook method for rendering the target images protected function renderImages() { $output = '<div id="slider">'; foreach ($this->images as $image) { $output .= '<img src="' . $image . '">'; } $output .= "</div>"; return $output; } // Hook method for including jQuery protected function renderjQuery() { return <<<ENDHTML <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"> </script> ENDHTML; } // Hook method for including the slider's dependencies // (overridden by subclasses) abstract protected function renderDependencies(); // Hook method for rendering a sliding effect (overridden by // subclasses) abstract protected function renderEffect(); // Template method for rendering the slider public final function render() { return $this->renderImages() . $this->renderjQuery() . $this->renderDependencies() . $this->renderEffect(); } } 

Это определенно выглядит лучше. Поскольку метод render() теперь разбит на более мелкие, мелкозернистые шаги, его просто создать изящными реализациями.

Приведенный ниже подкласс является базовой оболочкой для слайдера на основе цикла:

 <?php namespace LibraryViewHelper; class Cycle extends AbstractSlider { protected function renderDependencies() { return <<<ENDHTML <script src="http://cloud.github.com/downloads/malsup/cycle/jquery.cycle.all.latest.js"> </script> ENDHTML; } protected function renderEffect() { return <<<ENDHTML <script> jQuery(document).ready(function () { jQuery("#slider").cycle({fx: "fade"}); }); </script> ENDHTML; } } 

И этот оживляет слайдер на основе орбиты:

 <?php namespace LibraryViewHelper; class Orbit extends AbstractSlider { protected function renderDependencies() { return <<<ENDHTML <link rel="stylesheet" href="/public/css/orbit.css"> <script src="/public/js/orbit.min.js"> </script> ENDHTML; } protected function renderEffect() { return <<<ENDHTML <script> jQuery(document).ready(function () { jQuery("#slider").orbit(); }); </script> ENDHTML; } } 

Теперь, когда эти подтипы предоставляют пользовательские реализации для renderDependencies() и renderEffect() , каждый плагин может независимо отображаться в браузере с помощью сценария, подобного следующему:

 <?php $orbit = new Orbit($images); echo $orbit->render(); $cycle = new Cycle($images); echo $cycle->render(); 

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

Заключительные замечания

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

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

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

Изображение через Fotolia