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)

  1. Максим

    Спасибо за статью! Наконец-то смог понять принцип работы шаблона. Википедия идёт лесом…

  2. Алексей

    Спасибо за статью! А можете подробнее объяснить зачем он нужен? Т.е. из примера он выглядит как обёртка, которая делегирует поведение конкретной реализации класса. В принципе, если все классы реализуют один интерфейс, то почему не работать с ними напрямую и какая в нём польза? Как-то не очевидно… Ведь по сути результат остаётся тот же:

    $configModel = ‘INI’;
    if ($configModel == ‘INI’) {
    $config = new Config_Config(new Config_INI());
    } elseif ($configModel == ‘XML’) {
    $config = new Config_Config(new Config_XML());
    } else {
    // …
    }

    • dolphin

      Ну, не совсем то же самое. Эта логика, которая строит объект заданного типа, выносится в отдельный класс, который называется фабрикой. Автор рассмотрел здесь шаблон «стратегия» и, хотя этот вопрос очень плотно пересекается с шаблоном «фабрика», тем не менее, это два разных шаблона, каждый из которых решает свою задачу. Так что автору +1 :), хотя, как показывает практика, подобные статьи полезны как читающим, так и пишущим :)

  3. Дмитрий

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *