«Strategy Pattern. Просто о простом» или почему я хожу на собеседования «PHP Junior» ради fun’а

от автора

Приветствие

Всем привет! В этот радостный и, достаточно, теплый пятничный денек приключилась у меня (процитирую в более приятном варианте) «рука-лицо». Честно говоря, приключается сие действие достаточно часто, но, по обыкновению, вызывается оно от ощущения:

Боже, какой же я тупой.

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

Не подглядывать

Зная, что в электронной кладези знаний стопроцентно имеется статья «Шаблон проектирования Стратегия», с примером на C++, который скорее всего взят из книжки GoF, и на всех известных языках программирования (а вдруг и включая эзотерические), моей задачей стало: не подглядывая, упихать схожий пример из статьи в концепцию паттерна. Поглядим, что нужно сделать и что получилось.

Пилите, Шура, пилите

На мой взгляд, оперировать конкретными типами классов, не есть хорошо: в данном случа мы обладаем не только знанием о интерфейсе класса, но и о реализации, включаещей обычно ворох дополнительных методов (часто открытых === pulic). Вместо этого, предлагается подход, основанный на интерфейсах, так что первым шагом рефакторинга примера будет выделение оных:

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(), $param); } 

«Согласен», отвечу я тебе, внимательный читатель, на твой незримый вопрос:

Каждый раз вызывается конструктор? Может синглтон или статичная переменная функции, не?

Но это уже вопросы следующего рефакторинга. Вернемся к предметной области примера: нам понадобится сущности для валидации, к примеру, почтового ящика (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.

Заключение

Боюсь, эта статья будет образчиком, как, о казалось бы, простых вещах, можно «налить столько воды». Но в одном я уверен: будет здорово, если после этой статьи те из нас, кто по какой-либо причине пренебрегал или боялся познакомиться с паттернами проектирования, победит свой страх.

В конце концов: знание лучше неведения, а свет лучше темноты. Спасибо и удачных всем выходных!

P.S. Да, это моя первая статья, так что: поздравления, критика, оскорбления, ошибки\очепятки принимаются в личку.

ссылка на оригинал статьи http://habrahabr.ru/post/176505/


Комментарии

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

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