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

в 14:05, , рубрики: patterns, patterns and practices, php, php5, Программирование, Проектирование и рефакторинг, метки: , , ,

Приветствие

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

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

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

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

Зная, что в электронной кладези знаний стопроцентно имеется статья «Шаблон проектирования Стратегия», с примером на 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. Да, это моя первая статья, так что: поздравления, критика, оскорбления, ошибкиочепятки принимаются в личку.

Автор: maxvodo

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js