Pattern Strategy — «Стратегия»
Это ни что иное, как репост заметки, некогда опубликованной в старом блоге. Заметка переносится без каких-либо правок или доработок.
Strategy – это поведенческий шаблон проектирования, также известным как Policy, применимый там, где для решения одной и той же задачи могут использоваться различные алгоритмы. Важным моментом является реализация взаимозаменяемости алгоритмов.
Чтобы было понятней, рассмотрим более или менее живой пример. Допустим, ваше приложение должно уметь работать с несколькими типами конфигурационных файлов: XML, INI и т.п. В действительности, набор может быть каким угодно. Мы можем убрать один из типов конфигурационных файлов или наоборот добавить. Это не должно стать причиной того, что нам придется переписывать код приложения.
И так, представим, что на каждый алгоритм работы с определенным типом конфига у нас описан свой класс. Каждый класс обладает своим набором методов и свойств, то есть имеет свой интерфейс доступа, отличный от других классов набора. Замена одного алгоритма на другой станет непосильной задачей. Придется переписать весь код, где описана работа с конфигурационными файлами, чтение, запись или другие операции.
В коде у нас могло бы быть нечто вот такое:
<?php
$configModel = 'INI';
if ($configModel == 'INI') {
$config = new Config_INI();
} elseif ($configModel == 'XML') {
$config = new Config_XML();
} else {
// ...
}
?>
Так могла бы выглядеть инициализация объекта нужного нам класса. Несложно представить, чего будет стоить попытка изменить количество алгоритмов. Да чего уж там, переименование класса создаст не меньше проблем. К этому добавьте код, который работает с интерфейсом объекта и перспектива станет совсем унылой
Теперь попробуем сделать тоже, но уже по умному, с использованием поведенческого шаблона проектирования Strategy.
Для начала опишем код интерфейса, который будет имплементироваться всеми классами алгоритмов работы с конфигурационными файлами.
<?php
interface Config_Interface
{
/**
* Get contents file of config
*
* @param string $file
* @return boolean
*/
public function read($file);
/**
* Put contents file of config
* @param mixed $data
* @return boolean
*/
public function write($data);
/**
* Return value by key
* @param string $key
* @return mixed
*/
public function get($key);
}
?>
Интерфейс декларирует три основных метода (на деле их может быть больше, но для примера хватит и трех), которые должны быть реализованы каждым алгоритмом, входящим в набор. Эти методы будут тем самым единым интерфейсом, для работы с объектами классов. Мы загоняем себя в жесткие рамки, чтобы потом нам хорошо жилось.
Далее создаем класс для работы, например, с конфигами XML формата. Логику я опускаю, так как речь не о том, как парсить XML файлы. Главное, понять суть наших действий.
<?php
class Config_XML implements Config_Interface
{
/**
* (non-PHPdoc)
* @see Config_Interface#read()
*/
public function read($file) {
// ...
$this->toArray($data);
}
/**
* (non-PHPdoc)
* @see Config_Interface#write()
*/
public function write($data) {
}
/**
* (non-PHPdoc)
* @see Config_Interface#get()
*/
public function get($key) {
}
/**
* Convert contents file of config to assoc array
*
* @param mixed $data
* @return array
*/
private function toArray($data) {
}
}
?>
Как видите, интерфейс обязывает нас описать три публичных метода, объявленных в нем. Но так как каждый тип конфигурационных файлов требует своей логики работы с данными, помимо этих трех методов, в каждом классе алгоритма у нас будет свой набор дополнительных. Одним из таких можно сделать метод, который преобразует прочитанные из конфига данные в ассоциативный массив или нечто подобное.
Ну а теперь пример класса, реализующего паттерн Strategy:
<?php
class Config_Config
{
/**
* Instance of config driver
* @var Config_Interface
*/
private $_instance;
public function __construct(Config_Interface $instance) {
$this->_instance = $instance;
}
public function load($file) {
$this->_instance->read($file);
}
public function put($data) {
$this->_instance->write($data);
}
public function get($key) {
$this->_instance->get($key);
}
}
?>
И сразу пример использования:
<?php
$config = new Config_Config( new Config_XML() );
$config->load('filename.xml');
?>
Начнем по порядку. Во-первых, вы наверное заметили, что у классов и интерфейса немного странные имена. Это сделано с расчетом использования прелестей автоматической загрузки классов с помощью __autoload(). Так наше приложение станет более изящным.
Теперь рассмотрим инициализацию объекта от класса стратегии. В качестве параметра конструктора мы передаем объект нужного нам драйвера конфига, в данном случае Config_XML(). Конструктор класса Config_Config может принимать в качестве параметра только объекты имплементирующие интерфейс Config_Interface. Это еще одни рамки, в которые мы загоняем себя для достижения вселенского счастья. За счет этого мы точно знаем, что класс драйвера содержит методы read(), write() и get(). Методы самой стратегии являются оберткой для них.
В итоге мы получили единый интерфейс доступа ко всем алгоритмам набора. Да, где-то нам придется хорошенько извернуться, реализуя логику драйверов таким образом, чтобы она удовлетворяла наложенным нами же ограничениям, но кому сейчас легко?
Можно пойти дальше и сделать объект класса Config_Config доступным глобально. Для этого задействуйте паттерн проектирования Registry, о котором я писал ранее.
Надеюсь, был полезен. Успехов!
Комментарии (4)
Спасибо за статью! Наконец-то смог понять принцип работы шаблона. Википедия идёт лесом…
Спасибо за статью! А можете подробнее объяснить зачем он нужен? Т.е. из примера он выглядит как обёртка, которая делегирует поведение конкретной реализации класса. В принципе, если все классы реализуют один интерфейс, то почему не работать с ними напрямую и какая в нём польза? Как-то не очевидно… Ведь по сути результат остаётся тот же:
$configModel = ‘INI’;
if ($configModel == ‘INI’) {
$config = new Config_Config(new Config_INI());
} elseif ($configModel == ‘XML’) {
$config = new Config_Config(new Config_XML());
} else {
// …
}
Ну, не совсем то же самое. Эта логика, которая строит объект заданного типа, выносится в отдельный класс, который называется фабрикой. Автор рассмотрел здесь шаблон «стратегия» и, хотя этот вопрос очень плотно пересекается с шаблоном «фабрика», тем не менее, это два разных шаблона, каждый из которых решает свою задачу. Так что автору +1 :), хотя, как показывает практика, подобные статьи полезны как читающим, так и пишущим :)
Спасибо за пояснение паттерна. Думаю, нужно дополнить, что дынный шаблон предусматривает, что клиентский код сам будет знать и решать какой именно класс в данный момент нужно «скормить» конструктору.