Алексей Гоголев
Strategy Pattern в PHP
Для каких случаев предназначен Strategy Pattern?
Бывают такие ситуации, когда решение задачи требует разбора прямо таки бесконечного множества случаев. Решая такие проблемы «в лоб» мы получаем такие строки:
Написание такого кода требует огромного внимания. Необходимо четко продумать какие будут случаи, как эти случаи задать, и что в каждом случае делать. При большом количестве вариантов код становиться потенциальным источником багов. Кроме того, если в будущем понадобиться что-либо изменить в программе, то разбираться, где именно тот случай, который нужно подправить можно очень долго (если код тщательно не откоментирован).
Возникает потребность упростить код, сделать его более читаемым и прозрачным. Рассмотрим на примере как это сделать, используя Strategy Pattern.
Основная идея
Идея Strategy Pattern состоит в том, чтоб создать набор классов доступ к методам которых, осуществлялся бы одинаково (как эти классы будут использоваться — увидим дальше). Структура этого набора классов должна быть такова:
1. один абстрактный класс, который содержит одинаковые для каждого случая методы
2. классы (по классу на каждый случай), которые наследуют абстрактный класс и предназначены для конкретного случая (т.е. количество классов == количество случаев).
Классы-наследники содержат методы, для решения поставленной задачи в конкретном случае (все классы-наследники имеют одинаковое количество методов и эти методы называються одинаково. Т.е. если в одном из классов есть метод count, то во всех остальных метод count тоже есть). Таким образом, для любого случая необходимо взять соответствующий ему класс, и вызвать нужные методы. Та часть кода, где будут выполняться методы класса, не зависит от случая, т.к. методы во всех классах называются одинаково.
Пример
В этом примере будем проверять корректность заполнения регистрационной формы. Для простоты возьмем четыре поля. Пользователь должен ввести имя (user), пароль (password), подтверждение пароля (confirm password) и свою электропочту (email). Наша задача проверить корректно ли пользователь заполнил эти четыре поля. Если бы код писался «в лоб», проверка поля user выглядела бы примерно так:
<?
if ( isset ($_POST['submit']) )
{
if ( strlen($user) < '6' )
{
echo ('Username is too short');
}
else if ( $pass != $conf )
{
echo ('Passwords do not match');
}
else if ( $fullmoon == true )
{
// и т.п. и т.д.
}
}
?>
Построим набор классов, о котором шла речь выше. Не пугайтесь, что кода много – он простой и читаемый. Первым идет абстрактный класс, далее три класса-наследника, имеющие одинаковую структуру.
Абстрактный класс
<?php
class Validator {
// private $errorMsg сохраняет сообщение об ошибке,
// если поле заполнено не корректно
var $errorMsg;
// конструктор
function Validator ()
{
$this->errorMsg=array();
$this->validate();
}
// абстрактный метод, предназначенный для наследования
function validate() {}
// добаляет сообщение о ошибке в массив
function setError ($msg)
{
$this->errorMsg[]=$msg;
}
// возвращает значение true, если массив $errorMSG
// не содержит сообщений о ошибке (пустой),
// в противном случае возвращает false
function isValid ()
{
if ( isset ($this->errorMsg) )
{
return false;
}
else
{
return true;
}
}
// Возвращает последний елемент из
// массива $errorMsg, и удаляет последний елемент.
function getError ()
{
return array_pop($this->errorMsg);
}
}
?>
Класс-наследник для поля user
<?php
class ValidateUser extends Validator {
//Private $user содержимое поля user
var $user;
//конструктор
function ValidateUser ($user)
{
$this->user=$user;
Validator::Validator();
}
// Проверяет, корректно ли заполнено поле user,
// если нет - пишем сообщение о ошибке в $errorMsg
function validate()
{
if (!preg_match('/^[a-zA-Z0-9_]+$/',$this->user ))
{
$this->setError('Username contains invalid characters');
}
if (strlen($this->user) < 6 )
{
$this->setError('Username is too short');
}
if (strlen($this->user) > 20 )
{
$this->setError('Username is too long');
}
}
}
?>
Класс-наследник для поля password
<?php
class ValidatePassword extends Validator {
//Private $pass содержимое поля password
var $pass;
//Private $conf содержимое поля confirm password
var $conf;
//конструктор
function ValidatePassword ($pass,$conf)
{
$this->pass=$pass;
$this->conf=$conf;
Validator::Validator();
}
//проверяет, корректно ли введен пароль
//и совпадает ли он с подтверждением.
//если нет - дописывает сообщение о ошибке в $errorMsg
function validate()
{
if ($this->pass!=$this->conf)
{
$this->setError('Passwords do not match');
}
if (!preg_match('/^[a-zA-Z0-9_]+$/',$this->pass ))
{
$this->setError('Password contains invalid characters');
}
if (strlen($this->pass) < 6 )
{
$this->setError('Password is too short');
}
if (strlen($this->pass) > 20 )
{
$this->setError('Password is too long');
}
}
}
?>
Класс-наследник для поля email
<?php
class ValidateEmail extends Validator {
//Private $email содержимое поля email
var $email;
//конструктор
function ValidateEmail ($email)
{
$this->email=$email;
Validator::Validator();
}
//проверяет, корректно ли введена электропочта
//если нет - дописывает сообщение о ошибке в $errorMsg
function validate()
{
$pattern = "/^([a-zA-Z0-9])+([.a-zA-Z0-9_-])".
"*@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-]+)+/";
if(!preg_match($pattern,$this->email))
{
$this->setError('Invalid email address');
}
if (strlen($this->email)>100)
{
$this->setError('Address is too long');
}
}
}
?>
Таким образом, есть абстрактный класс Validator содержащий конструктор, абстрактный метод validate() общие для всех случаев методы: setError(), isValid(). И есть классы-наследники: ValidateUser, ValidatePassword, ValidateEmail. Классы-наследники содержат конструктор и метод validate().
Схема:
Теперь посмотрим, как вся эта кухня будет работать.
<?php
if ( $_POST['register'] ) {
// если наша форма что-то содержит
// приступаем к обработке данных
// создаем в массиве по экземпляру классов-наследников
$v['u']=new ValidateUser($_POST['user']);
$v['p']=new ValidatePassword($_POST['pass'],$_POST['conf']);
$v['e']=new ValidateEmail($_POST['email']);
// проверяем в цикле корректность каждого поля
foreach($v as $validator)
{
if (!$validator->isValid())
{
while ($error=$validator->getError())
{
$errorMsg.="<li>".$error."</li>n";
}
}
}
//если есть ошибки - выводим их
if (isset($errorMsg))
{
print ("<p>There were errors:<ul>n".$errorMsg."</ul>");
}
else
{
print ('<h2>Form Valid!</h2>');
}
}
?>
Заключение
Итого, благодаря применению Strategy Pattern, вместо гигантского количества операторов elseif, мы имеем четкую структуру из классов. Это дает следующие преимущества:
1. Устранен потенциальный источник багов.
2. Теперь, если нам нужно проверить больше полей мы просто дописываем по классу на поле, в «исполнительном» коде ничего менять не нужно.
3. Каждому полю соответствует класс –- и если потребуется будет внести корректировки, нужно будет просто найти класс с именем поля, вместо того чтоб рыскать среди великого множества команд ifelse.
Спасибо за внимание, надеюсь SP пригодится вам в работе :)
ссылки по теме:
1. php architect’s Guide to PHP Design Patterns by Jason E. Sweat
2. The Stategy Pattern (php) создание рассылки писем с помощью Strategy Pattern
3. phpPatterns (php) Strategy Pattern для проверки корректности заполнения форм
4. Five common PHP design patterns (php) factory, singleton, observer, strategy и chain-of-command pattern
Последние комментарии:
Cainrus | |
Вы не правы насчёт того что можно обойтись с помощью одной функции, которая будет проверять поля с соответствием их с установленными проверочными переменными( регулярные выражения, длинна поля и т.д. ). А что если однажды добавится новое скрытое системное поле, которое нельзя выбирать самому? например, номер текущей сессии, которая может храниться где угодно, хоть в БД. По Вашему примеру придется, либо дополнять функцию дополнительными переменными( как максимальное/минимальное кол-во символов в строке, паттерны регулярных выражений ), а затем прописать логику в функции, что будет выглядеть не красиво и путать программиста, увеличивая возможность багов, усложнять читаемость. Приведённый автором статьи пример класса валидатора легко расширяем и каждый новый наследуемый валидатор содержит свою логику только на свой частный случай, что не делает из кода каши, ускоряет разработку/исправления/дополнение кода в будущем, повышает читаемость снижает вероятность появления багов. |
|
fadanys | |
>>формат вывода ошибок может различаться от одной страницы к другой ........ >Это как? Зачем? например в разных разделах сайта. В статьях этого сайта речь идет вообще о шаблонах проектирования. То есть подразумевает возможность использования решения в различных проектах. >> но ООП представляется более удобным, так как не требует создавать переменные в вызывающем скрипте ........... > А что по Вашему значит $v['a'] = new ValidateNNN(); ? я имею ввиду какие либо дополнительные стрктуры для хранения сообщений и результатов. >>А какое собственно назначение у классов? ............ >Предоставлять пользователю инструмент для построения собственных типов, отражающих конкретные понятия, которые НЕЛЬЗЯ прямо и естественно отразить при помощи встроенных типов. Использование новых типов должно быть таким же удобным и прозрачным, как использование встроенных типов, что достижимо при работе через интерфейсы. Хм.. а я всегда считал, что для использования инкапсуляции, наследования и полиморфизма. инкапсуляция подразумевает проверку данных, а наследование – возможность описания в базовом классе неизменяемых функций и соответственно неповторяемость кода, полиморфизм – использование одинаковых вызовов. |
|
!true | |
>по хорошему нужно выводить все сообщения о неправильном вводе, а не только последнее. то есть хранить в массиве или отдельных переменных. .............. Посмотрите на описание функции-члена повнимательнее:
<?php Если что, то передача $errmsg по ссылке подразумевает в определения функции-члена конкатенацию $errstr .= '...' (то есть $errmsg .= '.....') Никаких других переменных там не нужно. >формат вывода ошибок может различаться от одной страницы к другой ........ Это как? Зачем? > но ООП представляется более удобным, так как не требует создавать переменные в вызывающем скрипте ........... А что по Вашему значит $v['a'] = new ValidateNNN(); ? >возможно изменение внутреннего формата, внесением изменений только в один класс. ........... Но не в этом примере. >А какое собственно назначение у классов? ............ Предоставлять пользователю инструмент для построения собственных типов, отражающих конкретные понятия, которые НЕЛЬЗЯ прямо и естественно отразить при помощи встроенных типов. Использование новых типов должно быть таким же удобным и прозрачным, как использование встроенных типов, что достижимо при работе через интерфейсы. Почему в примере не нужны классы? Потому, что абсолютно любое поле html-формы можно прямо представить при помощи уже встроенных в php типов. Потому, что в php уже присутствуют операции приведения базовых типов. И потому, что в php уже реализованы механизмы валидации этих типов. |
|
fadanys | |
2!true структурное программирование — это уже лучше. но именно по этому коду: по хорошему нужно выводить все сообщения о неправильном вводе, а не только последнее. то есть хранить в массиве или отдельных переменных. Учитывать, что формат вывода ошибок может различаться от одной страницы к другой. все это можно сделать и используя функции (я вовсе не против такого подхода), но ООП представляется более удобным, так как не требует создавать переменные в вызывающем скрипте, возможно изменение внутреннего формата, внесением изменений только в один класс. > Для классов есть вполне конкретное назначение. А какое собственно назначение у классов? |
|
!true | |
*(возможно, нужно пофиксить) В строке с валидацией пароля скрипт урезал бэкслэш перед знаком доллара в регулярном выражении. |
|
Обсудить (комментариев: 9)