Приветствие
Всем привет! В этот радостный и, достаточно, теплый пятничный денек приключилась у меня (процитирую в более приятном варианте) «рука-лицо». Честно говоря, приключается сие действие достаточно часто, но, по обыкновению, вызывается оно от ощущения:
Боже, какой же я тупой.В этот раз меня посетило несколько другое чувство, и, как я убедился, не только меня. Чувство это напомнило мне об одном из моих собеседований, где меня попросили написать скелет паттерна «Декоратор», который оказался в личном представлении интервьювера совершенно другим, нежели его классическое толкование.
Дабы не разочаровать и не ввести мозг читателя в «когнитивный диссонанс» уточню, что в этой статье пойдет речь о «гипотетическом» собеседовании в этой компании, так, если бы меня попросили рассказать о паттерне «Стратегия». Как и в предыдущем случае, мой ответ выглядел бы в глазах интервьювера как минимум сомнительно.
Не подглядывать
Зная, что в электронной кладези знаний стопроцентно имеется статья «Шаблон проектирования Стратегия», с примером на C++, который скорее всего взят из книжки GoF, и на всех известных языках программирования (а вдруг и включая эзотерические), моей задачей стало: не подглядывая, упихать схожий пример из статьи в концепцию паттерна. Поглядим, что нужно сделать и что получилось.
Пилите, Шура, пилите
На мой взгляд, оперировать конкретными типами классов, не есть хорошо: в данном случа мы обладаем не только знанием о интерфейсе класса, но и о реализации, включаещей обычно ворох дополнительных методов (часто открытых === public). Вместо этого, предлагается подход, основанный на интерфейсах, так что первым шагом рефакторинга примера будет выделение оных:
interface IValidator {
/**
* @param Mixed|stdClass $validatorParam
* @return Boolean
*/
function isValid($validatorParam);
}
Я позволил себе немного расслабиться и поэтому код будет содержать минимальное количество комментариев (на самом деле код содержит минимально допустимое, в моей IDE, количество комментариев). Итого: сущность, которая способна выдать вердикт о валидности по переданному аргументу. Стоит написать, что в данном месте, как и вас, меня начинает передергивать:
- Количество аргументов строго фиксируется (переменная массив не наш метод)
- Проблему выше не решить перегрузкой функции (да, C++ смотрит на PHP как на...)
- Тип аргумента не определен (хотя для многих программистов PHP это не проблема)
function Validate(IValidator $validateStrategy, $param) {
return $validateStrategy->isValid($param);
}
А адаптация нашего желания для валидации, например, имени пользователя будет выглядеть следующим образом:
function ValidateName($userName) {
return Validate(new NameValidator(), $userName); // obsolete $param, fixed by domage@habrahabr
}
«Согласен», отвечу я тебе, внимательный читатель, на твой незримый вопрос:
Каждый раз вызывается конструктор? Может синглтон или статичная переменная функции, не?Но это уже вопросы следующего рефакторинга. Вернемся к предметной области примера: нам понадобится сущности для валидации, к примеру, почтового ящика (email), и отвечающие на вопросы: этот адрес Hoho? этот адрес не слишком короткий или длинный?
class HohoEmailValidator implements IValidator {
public function isValid(/*String*/ $email) {
return $email === 'Hoho'; // best comparison of ever
}
}
class LengthEmailValidator implements IValidator {
public function isValid(/*String*/ $email) {
return strlen($email) > 5;
}
}
class LengthMaxEmailValidator implements IValidator {
public function isValid(/*String*/ $email) {
return strlen($email) <= 100500;
}
}
Так, как наша функция Validate() не позволяет использовать коллекцию (список, массив) валидаторов, то придется расширить её функционал использованием коллекции (списка, массива) валидаторов как аргумента:
function IsValidByStrategy(/*Collection of IValidators*/ $strategyCollection, /*Any type*/ $param) {
foreach($strategyCollection as $strategy) {
if($strategy instanceof IValidator) { // в наше время доверять нельзя никому -_-
if(!$strategy->isValid($param))
return false;
}
}
return true;
}
О да, это приятное шевеление волос на голове:
- Не отслеживается состояние, когда коллекция валидаторов пуста
- Нельзя изменить поведение функции: она вываливается при первом false функции isValid()
// Возврашает true, если
function IsValidEmailStrong($email = 'habr@habr.ru') {
return IsValidByStrategy(array(
new HohoEmailValidator(), // это "нормальный" адрес
new LengthEmailValidator() // и он больше 5 символов
), $email);
}
Логика проверки получилась щикарная (< — это не ошибка):
- Email должен являться строкой 'Hoho' — что уже бредово
- Ну, и даже если адрес будет действительно 'Hoho', то он никогда не будет больше 5 символов
- Но, следует отметить, что проверка с помощью LengthEmailValidator в данном контексте никогда не будет выполнена
Дополнение
Чтобы сделать пример менее идиотским (помним про LengthEmailValidator), еще разок применим паттерн стртегия, но применительно к изменению поведения функции IsValidByStrategy() и реализуем следующий функционал:
interface IsValidReturnRule {
/**
* @param Boolean $validateResult
* @param Boolean $validateResultByStep
* @return Boolean
*/
function validateStep(&$validateResult, $validateResultByStep);
/**
* @return Boolean
*/
function initialize();
}
class ValidReturnRuleAny implements IsValidReturnRule {
public function validateStep(&$validateResult, $validateResultByStep) {
$validateResult |= $validateResultByStep;
return $validateResult;
}
public function initialize() {
return false;
}
}
function IsValidByStrategyByRetRule(/*Collection of IValidators*/ $strategyCollection, /*Any type*/ $param, IsValidReturnRule $retStrategy) {
$result = $retStrategy->initialize();
foreach($strategyCollection as $strategy) {
if($strategy instanceof IValidator) {
if($retStrategy->validateStep($result, $strategy->isValid($param)))
return $result;
}
}
return $result;
}
Итого мы получили:
- Возможность изменения поведения при валидации
- Возможность изменения поведения процесса самой валидации
class ValidReturnRuleAny implements IsValidReturnRule {
public function validateStep(&$validateResult, $validateResultByStep) {
$validateResult |= $validateResultByStep;
return $validateResult;
}
public function initialize() {
return false;
}
}
function IsValidEmailSoft($email = 'habr@habr.ru') {
return IsValidByStrategyByRetRule(array(
new HohoEmailValidator(),
new LengthEmailValidator()
), $email, new ValidReturnRuleAny());
}
Из-за использования стратегии обработки результата стратегии валидации, функция IsValidEmailSoft будет не возражать о любых адресах, имеющих длину не более 100500 символов, т.е.
- IsValidEmailStrong()
- Адрес невалиден, если любая из стратегий валидации вернула false
- IsValidEmailSoft()
- Адрес валиден, если любая из стратегий валидации вернула true
Что нужно помнить
Любое проектное решение реализующее изящное решение (включая особенности языка) общей задачи, имеет ограничения использования. В этом смысле прекрасно стукрурирована книжка Design patterns авторством GoF.
Заключение
Боюсь, эта статья будет образчиком, как, о казалось бы, простых вещах, можно «налить столько воды». Но в одном я уверен: будет здорово, если после этой статьи те из нас, кто по какой-либо причине пренебрегал или боялся познакомиться с паттернами проектирования, победит свой страх.
В конце концов: знание лучше неведения, а свет лучше темноты. Спасибо и удачных всем выходных!
Upd:
- #bugfix from domage
P.S. Да, это моя первая статья, так что: поздравления, критика, оскорбления, ошибки\очепятки принимаются в личку.