Евгений Загородний
Factory Pattern в PHP
Нередко в программе бывает необходимо произвести несколько однотипных действий. В таких случаях целесообразно пользоваться 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->r = $red;
$this->g = $green;
$this->b = $blue;
}
function getRgb() {
return sprintf('#%02x%02x%02x', $this->r, $this->g, $this->b);
}
}
?>
sprintf
sprintf возрвращает строку, обработанную в соответствии с заданными параметрами форматирования. В данном примере "%02" — указание минимальной ширины выводимого числа, т. е. вместо 5 будет выводиться 05; x — вывод числа в шеснадцатеричном формате.
Как работает класс Color? Допустим, нам необходимо представление в шеснадцатеричном формате цвета с красной, зеленой и синей компонентами, равными соответственно 255, 128 и 64.
Тогда мы:
- создаем объект класса Color, инициализируя его тремя указанными значениями;
- вызываем для метод getRgb() этого объекта, который и возвращает нужную нам строку.
Выглядит это так.
<?php
$color = new Color(255, 128, 64);
$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->r = $red;
if ($red < 0 || $red > 255) {
trigger_error("color '$red' out of bounds");
}
$this->g = $green;
if ($green < 0 || $green > 255) {
trigger_error("color '$green' out of bounds");
}
$this->b = $blue;
if ($blue < 0 || $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->r = this->validateColor($red);
$this->g = this->validateColor($green);
$this->b = this->validateColor($blue);
}
function validateColor($color) {
if ($color < 0 || $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_name, 0, $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_name, 0, $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 обработку новой ситуации.
Ссылки по теме
(в скобках – язык, на котором приведены примеры)- The Factory Pattern (Java)
- Pattern Summaries: Abstract Factory Pattern by Mark Grand (Java)
- Principles, Patterns, and Practices: The Factory Pattern by Robert C. Martin (Java)
- The Abstract Factory Design Pattern by Antonio Garcia (no code)
- Abstract Factory pattern (Delphi)
- The Factory Method (Creational) Design Pattern by Gopalan Suresh Raj (Java)
Последние комментарии:
Владимир | |
Первый пример показывает проблему. Второй – решает ее в рамках шаблона Factory. |
|
где здесь фабрика | |
и где здесь фабрика? два каких-то левых примера | |
BOLK | |
http://lv2.php.net/manual/en/language.oop5.patterns.php | |
Обсудить (комментариев: 3)