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

    Приветствие


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

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

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

    Зная, что в электронной кладези знаний стопроцентно имеется статья «Шаблон проектирования Стратегия», с примером на 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:
    P.S. Да, это моя первая статья, так что: поздравления, критика, оскорбления, ошибки\очепятки принимаются в личку.
    • –18
    • 43k
    • 5
    Share post

    Comments 5

      +17
      Когнитивный диссонанс. В приветствии про собеседования и «Декоратор», а в статье — про реализацию стратегии. И каков же ответ на вопрос, почему вы ходите на собеседования ради фана? Кто здесь?
        –9
        Перефразирую: если бы я пришел на собеседование в «одну компании по написанию различных web систем» из приведенной статьи, то мы бы разошлись в определениях паттерна «Стратегия» и история бы повторилась.

        Часть ответа в самом вопросе, оставшаяся часть: скилл общения, скилл оценки своих знаний и выход из зоны комфорта. А на счет:
        Кто здесь?
        — мы с вами.
        +3
        будет здорово, если после этой статьи те из нас, кто по какой-либо причине пренебрегал или боялся познакомиться с паттернами проектирования, победит свой страх.

        Каким образом статься должна сподвигнуть изучение паттернов?
        Вы применили паттерн стратегию 2 раза к абстрактной задаче, и что это должно показать?
        Не спорю, получилось замечательно, но при чём тут заголовок статьи?
          –1
          Уже спустя некоторое время я уже не могу ответить, чем меня так привлекло название статьи)
          Потаённая мысль же была такой: не нужно самостоятельно императивно изобретать паттерны — иначе, как минимум, терминология не сойдется.
          При написании следующей статьи учту ваши справедливые замечания и буду более последователен и логичен при изложении мыцлей (< — нет ошибки).
            0
            … мыцлей (< — нет ошибки).

            Это что значит?

        Only users with full accounts can post comments. Log in, please.