Шаблоны проектирования: пособие для начинающих

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

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

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

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

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

Есть три основных вида шаблонов проектирования:

  • структурные;
  • порождающие;
  • поведенческие.

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

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

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

Зачем их использовать?

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

Пример.

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

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

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

<?php  
	class StrategyAndAdapterExampleClass {  
	    private $_class_one;  
	    private $_class_two;  
	    private $_context;  
	      
	    public function __construct( $context ) {  
	            $this->_context = $context;  
	    }  
	      
	    public function operation1() {  
	        if( $this->_context == "context_for_class_one" ) {  
	            $this->_class_one->operation1_in_class_one_context();  
	        } else ( $this->_context == "context_for_class_two" ) {  
	            $this->_class_two->operation1_in_class_two_context();  
	        }  
	    }  
	}

Довольно просто, не правда ли? Теперь давайте подробнее рассмотрим шаблон стратегии.

Шаблон Стратегия

Шаблон Стратегия

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


В приведенном выше примере стратегия основана на том, что каково бы не было значение $context класс все равно иснстанциируется. Если в зависимости от контекста назначается class_one, то class_one и будет использоваться. И наоборот.

Здорово, но где я могу использовать это?

переформулировать контекст

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

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

<?php  
	class User {  
	      
	    public function CreateOrUpdate($name, $address, $mobile, $userid = null)  
	    {  
	        if( is_null($userid) ) {  
	            // это значит, что такого пользователя еще не существует, создать новую запись  
	        } else {  
	            // это значит, что такой пользователь уже существует, просто обновить запись, используя предоставленные данные  
	        }  
	    }  
	}

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

Шаблон Адаптер

Шаблон Адаптер

«Шаблон Адаптер является структурным шаблоном проектирования, который позволяет перепрофилировать класс с различным интерфейсом. Это в свою очередь позволяет использовать его в системе, где присутствуют различные методы вызова».


Это также позволяет изменять некоторые из входящих клиентских классов, перестраивая их в нечто совместимое с функциями Адаптера.

Как я могу использовать это?

Адаптер

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

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

Сравните эти две реализации.

Без применения Адаптера:

<?php  
	$user = new User();  
	$user->CreateOrUpdate( //inputs );  
	  
	$profile = new Profile();  
	$profile->CreateOrUpdate( //inputs );

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

А лучше бы.

В то же время можно было бы сделать что-то наподобие этого:

<?php  
	$account_domain = new Account();  
	$account_domain->NewAccount( //inputs );

Теперь у нас есть класс-оболочка, которая станет нашим классом аккаунта домена:

<?php  
	class Account()  
	{  
	    public function NewAccount( //inputs )  
	    {  
	        $user = new User();  
	        $user->CreateOrUpdate( //subset of inputs );  
	          
	        $profile = new Profile();  
	        $profile->CreateOrUpdate( //subset of inputs );  
	    }  
	}

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

Шаблон Фабричный метод

Шаблон Фабричный

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


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

Когда я могу это использовать?

применения фабричного метода

Наиболее подходящим шаблон Фабричный метод является тогда, когда у вас имеется несколько различных вариантов одного объекта. Допустим, что у вас есть класс кнопки; этот класс имеет несколько вариаций, таких как ImageButton, InputButton и FlashButton.

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

Давайте приступим к созданию наших трех классов:

<?php  
	abstract class Button {  
	    protected $_html;  
	      
	    public function getHtml()  
	    {  
	        return $this->_html;  
	    }  
	}  
	  
	class ImageButton extends Button {  
	    protected $_html = "..."; //Это может быть любой код HTML, который вы захотите использовать для кнопки-картинки  
	}  
	  
	class InputButton extends Button {  
	    protected $_html = "..."; // Это может быть любой код HTML, который вы захотите использовать для обычной кнопки (<input type="button"... />);  
	}  
	  
	class FlashButton extends Button {  
	    protected $_html = "..."; // Это может быть любой код HTML, который вы захотите использовать для флэш-кнопки
	
	}

Теперь мы можем создать класс нашей «фабрики»:

<?php  
	class ButtonFactory  
	{  
	    public static function createButton($type)  
	    {  
	        $baseClass = 'Button';  
	        $targetClass = ucfirst($type).$baseClass;  
	   
	        if (class_exists($targetClass) && is_subclass_of($targetClass, $baseClass)) {  
	            return new $targetClass;  
	        } else {  
	            throw new Exception("The button type '$type' is not recognized.");  
	        }  
	    }  
	}

Мы можем использовать этот код следующим образом:

$buttons = array('image','input','flash');  
	foreach($buttons as $b) {  
	    echo ButtonFactory::createButton($b)->getHtml()  
	}

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

Шаблон Декоратор

«Шаблон Декоратор является структурным шаблоном проектирования, который позволяет нам добавить новое или дополнительное поведение объекту в процессе выполнения, зависимо от ситуации».

Шаблон Декоратор

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

Шаблон позволяет также объединять нескольких декораторов для одного объекта — таким образом, вы не ограничиваетесь одним декоратором для каждого отдельного экземпляра.

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

Для реализации шаблона Декоратор мы можем выполнить следующие шаги:

  • Создать подкласс оригинального класса «Компонент» в классе «Декоратор»;
  • В классе «Декоратор» добавить как отдельное поле указатель подкласса «Компонент»;
  • Передать «Компонент» в конструктор «Декоратора» для инициализации указателя «Компонента»;
  • В классе «Декоратор» перенаправлять все методы «Компонент» к указателю «Компонент»;
  • В классе «Декоратор» переопределять любой(ые) метод(ы) «Компонент», поведение которого(ых) должно быть изменено.

Данные шаги более подробно описаны здесь.

Когда я могу это использовать?

Декоратор

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

Как раз здесь мы можем применить шаблон Декоратор.

Во-первых, давайте создадим несколько различных «декораторов», которые нам будут нужны:

  • Если мы находимся на главной странице и только что вошли в систему, нам нужна ссылка, которая заключена в теги <h2>.
  • Если мы находимся на другой странице и вошли в систему, нам нужна ссылка, которая заключена в теги underline.
  • Ссылка, заключенная в теги <strong>.

После того, как мы разобрались с нашими декораторами, мы можем начать их создавать.

<?php  
	class HtmlLinks {  
	    //некоторые методы, которые применимы для всех html-ссылок
	}  
	  
	class LogoutLink extends HtmlLinks {  
	    protected $_html;  
	      
	    public function __construct() {  
	        $this->_html = "<a href="logout.php">Logout</a>";  
	    }  
	      
	    public function setHtml($html)  
	    {  
	        $this->_html = $html;  
	    }  
	      
	    public function render()  
	    {  
	        echo $this->_html;  
	    }  
	}  
	  
	class LogoutLinkH2Decorator extends HtmlLinks {  
	    protected $_logout_link;  
	      
	    public function __construct( $logout_link )  
	    {  
	        $this->_logout_link = $logout_link;  
	        $this->setHtml("<h2>" . $this->_html . "</h2>");  
	    }  
	      
	    public function __call( $name, $args )  
	    {  
	        $this->_logout_link->$name($args[0]);  
	    }  
	}  
	  
	class LogoutLinkUnderlineDecorator extends HtmlLinks {  
	    protected $_logout_link;  
	      
	    public function __construct( $logout_link )  
	    {  
	        $this->_logout_link = $logout_link;  
	        $this->setHtml("<u>" . $this->_html . "</u>");  
	    }  
	      
	    public function __call( $name, $args )  
	    {  
	        $this->_logout_link->$name($args[0]);  
	    }  
	}  
	  
	class LogoutLinkStrongDecorator extends HtmlLinks {  
	    protected $_logout_link;  
	      
	    public function __construct( $logout_link )  
	    {  
	        $this->_logout_link = $logout_link;  
	        $this->setHtml("<strong>" . $this->_html . "</strong>");  
	    }  
	      
	    public function __call( $name, $args )  
	    {  
	        $this->_logout_link->$name($args[0]);  
	    }  
	}

Мы сможем использовать его затем следующим образом:

$logout_link = new LogoutLink();  
	  
	if( $is_logged_in ) {  
	    $logout_link = new LogoutLinkStrongDecorator($logout_link);  
	}  
	  
	if( $in_home_page ) {  
	    $logout_link = new LogoutLinkH2Decorator($logout_link);  
	} else {  
	    $logout_link = new LogoutLinkUnderlineDecorator($logout_link);  
	}  
	$logout_link->render();

Здесь мы имеем возможность увидеть, как можно комбинировать несколько декораторов, если мы в них нуждаемся.

Так как все декораторы прибегают к магической функции __call, мы все еще можем считать call методом первоначальной функции.

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

<strong><h2><a href="logout.php">Logout</a></h2></strong>

Одноэлементный шаблон

Одноэлементный шаблон

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


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

Когда я могу это использовать?

Одноэлементный шаблон 2

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

Представьте, что вы создали класс Session, который имитирует глобальный массив $ _SESSION. Поскольку этот класс нужно будет создать только один раз, мы можем применить одноэлементный шаблон следующим образом:

<?php  
	class Session  
	{  
	    private static $instance;  
	      
	    public static function getInstance()  
	    {  
	        if( is_null(self::$instance) ) {  
	            self::$instance = new self();  
	        }  
	        return self::$instance;  
	    }  
	      
	    private function __construct() { }  
	      
	    private function __clone() { }  
	      
	    //  все другие методы session, которые мы могли бы использовать
	    ...  
	    ...  
	    ...  
	}  
	  
	// вывод объекта session
	$session = Session::getInstance();

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

Заключение

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

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

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

Перевод статьи «A Beginner’s Guide to Design Patterns» был подготовлен дружной командой проекта Сайтостроение от А до Я.