developer.co.ua

Holy Copypasters
09.10.2006
Евгений Загородний

Factory Pattern в PHP 0.39

Нередко в программе бывает необходимо произвести несколько однотипных действий. В таких случаях целесообразно пользоваться Factory Pattern, о котором и пойдет речь. Рассматриваемые приемы довольно естественны, поэтому не удивляйтесь, если обнаружите, что пользовались ими раньше, не подозревая о Factory Pattern :)

Извлечение метода (extract method)

Чтобы продемонстрировать удобство использования Factory Pattern, рассмотрим простенький пример.

Задача. Разработать класс Color, выводящий RGB-цвет для HTML в шеснадцатеричном формате. Значения R, G, B передаются в качастве аргументов конструктору, а метод getRgb() возвращает строку с шеснадцатеричным представлением цвета.


Пока что все очень просто.

<?php
class Color {
   var 
$r 0;
   var 
$g 0;
   var 
$b 0;

   function 
Color($red 0$green 0$blue 0) {
      
$this->$red;
      
$this->$green;
      
$this->$blue;
   }

   function 
getRgb() {
      return 
sprintf('#%02x%02x%02x'$this->r$this->g$this->b);
   }
}
?>


sprintf
sprintf возрвращает строку, обработанную в соответствии с заданными параметрами форматирования. В данном примере "%02" — указание минимальной ширины выводимого числа, т. е. вместо “5” будет выводиться “05”; “x” — вывод числа в шеснадцатеричном формате.


Как работает класс Color? Допустим, нам необходимо представление в шеснадцатеричном формате цвета с красной, зеленой и синей компонентами, равными соответственно 255, 128 и 64.
Тогда мы:

Выглядит это так.

<?php
$color 
= new Color(25512864);
$hex_color color->getRgb();
echo 
$hex_color;
?>


Этот код выводит следующее:


Теперь усложним задачу. Грамотно написанный код должен корректно работать при любых входных данных. Однако приведенный класс Color не предусматривает проверку параметров на корректность, и если инициализировать создаваемый объект, скажем, отрицательным числом или вообще «левым» текстом, то его поведение будет непредсказуемо.

Добавим в наш класс проверку входных данных так, чтобы если один из параметров конструктора не являлся целым числом от 0 до 255, то выводилось сообщение об ошибке.

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

<?php
class Color {
   var 
$r 0;
   var 
$g 0;
   var 
$b 0;

   function 
Color($red 0$green 0$blue 0) {
      
$this->$red;
      if (
$red || $red 255) {
          
trigger_error("color '$red' out of bounds");
      }

      
$this->$green;
      if (
$green || $green 255) {
          
trigger_error("color '$green' out of bounds");
      }

      
$this->$blue;
      if (
$blue || $blue 255) {
          
trigger_error("color '$blue' out of bounds");
      }

   }

   function 
getRgb() {
      return 
sprintf('#%02x%02x%02x'$this->r$this->g$this->b);
   }
}
?>


trigger_error
trigger_error генерирует пользовательское сообщение об ошибке (error, warning или notice). Первый параметр — строка, содержащая сообщение об ошибке, второй — тип ошибки (E_USER_ERROR, E_USER_WARNING или E_USER_NOTICE). В нашем примере используется второй параметр по умолчанию — E_USER_NOTICE.

Ничего но бросается в глаза? Да! В этом коде одна и та же проверка повторяется три раза!
И хотя написать такую программу, используя Ctrl+C / Ctrl+V не представляет трудностей, она имеет существенный недостаток.

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

Вот тут-то и приходит на помощь Factory Pattern, а точнее, одна из его концепций — извлечение метода (extract method).

Извлечение метода
Когда имеются два или более участка кода, которые могут быть объединены, создавайте отдельный метод. Выбирайте имя метода, соответствующее его назначению. Извечение метода наиболее эффективно, когда однин и тот же участок кода повторяется несколько раз в методе (методах) класса.

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

<?php
class Color {
   var 
$r 0;
   var 
$g 0;
   var 
$b 0;

   function 
Color($red 0$green 0$blue 0) {
      
$this->this->validateColor($red);
      
$this->this->validateColor($green);
      
$this->this->validateColor($blue);
   }

   function 
validateColor($color) {
      if (
$color || $color 255) {
          
trigger_error("color '$color' out of bounds");
      } else {
         return 
$color;
      }

   }

   function 
getRgb() {
      return 
sprintf('#%02x%02x%02x'$this->r$this->g$this->b);
   }
}
?>


Эта версия класса работает в точности так же, как и предыдущая, но теперь недостатки устранены: код легко читаем и исправляем.

Обеспечиваем полиморфизм


Одним из принципов объектно-ориентированного программирования является полиморфизм, то есть использование одного интерфейса для различных методов решения задачи. Покажем, как этот принцип реализуется с помощью Factory Pattern.

Предположим, нам необходимо знать имя и фамилию пользователя. Для удобства позволим ввести ему данные в одном из двух форматов: имя фамилия или фамилия,имя.
Наша программа будет определять, в каком из двух форматов введены данные по наличию в вводимой строке пробела или запятой.

Задача. Разработать класс Namer, выводящий имя и фамилию отдельными строками. Строка, введенная пользователем передается в качестве аргумента конструктору а методы getName() и getSurname() возвращают соответственно имя и фамилию.

В чем же заключается полиморфизм? В том, что при использовании класса Namer не надо будет задумываться о том, какой же из форматов предпочел пользователь, — несмотря на то, что для разных форматов строка разбивается на имя и фамилию по-разному.

Итак, определим класс Namer.

<?php
class Namer {
   var 
$name;
   var 
$surname;
   function 
getName() { return $this->name; }
   function 
getSurname() { return $this->surname; }
}
?>


Как видим, интерфейсная часть полностью определена. Осталось реализовать два механизма обработки входной строки. Сделаем это мы в двух классах-наследниках SpaceNamer и CommaNamer.

<?php
// класс для обработки строки в формате "имя фамилия"
class SpaceNamer extends Namer {
   function 
SpaceNamer($full_name) {
      
$splitter_pos strpos($full_name' '); // находим пробел
      
$this->name substr($full_name0$splitter_pos); // все, что до пробела - это имя
      
$this->surname substr($full_name$splitter_pos+1); // после пробела - фамилия
   
}
}

// класс для обработки строки в формате "фамилия,имя"
class CommaNamer extends Namer {
   function 
CommaNamer($full_name) {
      
$splitter_pos strpos($full_name','); // находим запятую
      
$this->name substr($full_name$splitter_pos+1); // все, что до запятой - это фамилия
      
$this->surname substr($full_name0$splitter_pos); // после запятой - имя
   
}
}
?>


Оба класса-наследника готовы к использованию, каждый — для своего случая. А для того, чтобы не было необходимости в программе явно определять, какой из них использовать, создадим так называемый Factory class. Его метод getNamer и будет возвращать объект нужного нам класса, наследующего класс Namer.

<?php
class NamerFactory {
   function 
getNamer($full_name) {
      if (
false != strpos($full_name' ')) {
      
// в качестве разделителя использован пробел
         
return new SpaceNamer($full_name);
      } else if (
false != strpos($full_name',')) {
      
// в качестве разделителя использована запятая
         
return new CommaNamer($full_name);
      } else {
      
// не найден ни пробел, ни запятая - мы так не договаривались!
         
trigger_error("invalid name format");
      }
   }
}
?>



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

Вот как используется готовый Factory class.

<?php
$input1 
'James Bond';
$input2 'Иванов,Василий';

$factory = new NamerFactory;
$namer1 $factory->getNamer($input1);
$namer2 $factory->getNamer($input2);

echo 
'1st person - name: '.$namer1->getName().', surname: '.$namer1->getSurname().'<br>';
echo 
'2nd person - name: '.$namer2->getName().', surname: '.$namer2->getSurname();
?>


Как и следовало ожидать, выводится:



В дальнейшем, если нам понадобится модифицировать обработку входной строки в каком-то из случаев, достаточно будет изменить только один класс, сооиветствующий этому случаю.
Аналогично, для того чтобы обрабатывать большее количество ситуаций (например, мы решим, что если в строке отсутствует разделитель, то вся строка считается фамилией), нужно создать еще один класс, наследующий Namer, и добавить в метод getNamer класса NamerFactory обработку новой ситуации.

Ссылки по теме

(в скобках – язык, на котором приведены примеры)

1 2 3 4 5

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

Владимир
Первый пример показывает проблему.
Второй – решает ее в рамках шаблона Factory.

где здесь фабрика
и где здесь фабрика? два каких-то левых примера

BOLK
http://lv2.php.net/manual/en/language.oop5.patterns.php

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