developer.co.ua

Holy Copypasters
19.10.2006
Алексей Гоголев

Strategy Pattern в PHP 0.20

Для каких случаев предназначен 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) < )
        {
            
$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) < )
        {
            
$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().

Схема:

http://developer.co.ua/upload/Image/strategypattern-1.gif

Теперь посмотрим, как вся эта кухня будет работать.

<?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

1 2 3 4 5

Последние комментарии:

Cainrus
Вы не правы насчёт того что можно обойтись с помощью одной функции, которая будет проверять поля с соответствием их с установленными проверочными переменными( регулярные выражения, длинна поля и т.д. ). А что если однажды добавится новое скрытое системное поле, которое нельзя выбирать самому? например, номер текущей сессии, которая может храниться где угодно, хоть в БД.
По Вашему примеру придется, либо дополнять функцию дополнительными переменными( как максимальное/минимальное кол-во символов в строке, паттерны регулярных выражений ), а затем прописать логику в функции, что будет выглядеть не красиво и путать программиста, увеличивая возможность багов, усложнять читаемость.

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

fadanys

........

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

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

............

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

!true

..............

Посмотрите на описание функции-члена повнимательнее:
<?php
public static function validateText(&$text, &$errstr$sizeMin 1$sizeMax 65536$regexp NULL)
...
MYNAMESPACE::validateText$_POST['user'], $errmsg620 );
?>

Если что, то передача $errmsg по ссылке подразумевает в определения функции-члена конкатенацию $errstr .= '...' (то есть $errmsg .= '.....') Никаких других переменных там не нужно.

........
Это как? Зачем?

...........
А что по Вашему значит $v['a'] = new ValidateNNN(); ?

...........
Но не в этом примере.

............
Предоставлять пользователю инструмент для построения собственных типов, отражающих конкретные понятия, которые НЕЛЬЗЯ прямо и естественно отразить при помощи встроенных типов. Использование новых типов должно быть таким же удобным и прозрачным, как использование встроенных типов, что достижимо при работе через интерфейсы.

Почему в примере не нужны классы?
Потому, что абсолютно любое поле html-формы можно прямо представить при помощи уже встроенных в php типов. Потому, что в php уже присутствуют операции приведения базовых типов. И потому, что в php уже реализованы механизмы валидации этих типов.

fadanys
2!true
структурное программирование — это уже лучше.
но именно по этому коду:
по хорошему нужно выводить все сообщения о неправильном вводе, а не только последнее. то есть хранить в массиве или отдельных переменных. Учитывать, что формат вывода ошибок может различаться от одной страницы к другой.
все это можно сделать и используя функции (я вовсе не против такого подхода), но ООП представляется более удобным, так как не требует создавать переменные в вызывающем скрипте, возможно изменение внутреннего формата, внесением изменений только в один класс.


А какое собственно назначение у классов?

!true
*(возможно, нужно пофиксить) В строке с валидацией пароля скрипт урезал бэкслэш перед знаком доллара в регулярном выражении.

Обсудить (комментариев: 9)