К примеру, нужно нам создать приложение, которое в зависимости от типа операционной системы будет исполнять тот или иной код. Пример конечно притянутый за уши, для перла, но что же поделать, на википедии других нет.
Основной скрипт. Тут и намёка нет на Абстрактную фабрику и нашу усложнённую реализацию. При скриптовом подходе, вероятнее всего тут же была бы проверка на тип операционки. Мы пойдём более элегантным путём.
#!/usr/bin/perl
# файл ./test_factory.pl
use strict;
use Application;
my $app = Application->new;
$app->button->paint();
Далее идёт основной класс приложения. В нём уже видно, что мы собираемся использовать фабрику.
# ./Application.pm - основной класс приложения
package Application;
use Factory::GUIFactory; # Абстрактный класс фабрики
# конструктор
sub new {
my $self = bless {};
$self->_init();
return $self;
}
# начальная инициализа переменных объекта
sub _init {
my $self = shift;
# обращаемся к абстрактной фабрике
$self->{factory} = Factory::GUIFactory->create();
# на основе результата обращения к абстрактной фабрике
# вызываем конструктор конкретного продукта
$self->{button} = $self->{factory}->createButton();
}
# метод аксессор для нашей кнопки
sub button {
my $self = shift;
defined $self->{button}
? $self->{button}
: $self->_init()->{button};
}
1;
В классе Абстрактной фабрики объявляем интерфейсы для основных операций
package Factory::GUIFactory;
# ./Factory/GUIFactory.pm - класс Абстрактной фабрики. Объявляет
# интерфейс для операций, создающих абстрактные продукты
use feature ':5.10'; # заюзаем функцию given из perl 5.10
# тут уже будем использовать конкретные фабрики
use Factory::WinFactory;
use Factory::XFactory;
sub new { return bless {} }
sub createButton { die "Must implement this" }
# метод в котором происходит основная магия Абстрактной фабрики!
# выбираем ту или иную Конкретную фабрику в зависимости от условий
sub create {
given ($^O) {
when ("MSWin32") { return Factory::WinFactory->new }
when ("linux") { return Factory::XFactory->new }
default { die "Unknown OS" }
}
}
1;
Далее идут две Конкретные фабрики отвечающие за стыковку фабрики с Конкретным продуктом. Обычно, нужен только один экземпляр класса Конкретной фабрики поэтому для реализации данного класса лучше использовать паттерн Одиночка (Singleton), но об этом расскажу в другой статье.
package Factory::WinFactory;
# ./Factory/WinFactory.pm - Конкретная фабрика.
# Реализует операции, создающие конкретные объекты - продукты
use base "Factory::GUIFactory"; # базируемся на Абстрактной фабрике
use Product::WinButton; # выход на Конкретный продукт
sub new { return bless {} }
sub createButton { Product::WinButton->new }
1;
package Factory::XFactory;
# ./Factory/XFactory.pm - Конкретная фабрика.
# Реализует операции, создающие конкретные объекты - продукты
use base "Factory::GUIFactory"; # базируемся на Абстрактной фабрике
use Product::XButton; # выход на Конкретный продукт
sub new { return bless {} }
sub createButton { Product::XButton->new }
1;
и два класса реализации Конкретных продуктов
package Product::WinButton;
# ./Product/WinButton.pm - Конкретный продукт. Создаётся
# через Конкретную фабрику и никак иначе.
use base "Product::GUIButton"; # реализуем интерфейс Абстрактного продукта
# метод Конкретного продукта который мы скрыли за Абстрактной фабрикой
sub paint { print "I'm a WinButton\n" }
1;
package Product::XButton;
# ./Product/XButton.pm - Конкретный продукт. Создаётся
# через Конкретную фабрику и никак иначе.
use base "Product::GUIButton"; # реализуем интерфейс Абстрактного продукта
# метод Конкретного продукта который мы скрыли за Абстрактной фабрикой
sub paint { print "I'm a XButton\n" }
1;
Для классов Конкретных продуктов есть общий конкретный класс Абстрактного продукта на котором они основываются и который задаёт общий интерфейс методов. В перле конечно можно обойтись и без этого интерфейсного класса, т.к. декларативности для методов и атрибутов класса как таковой тут нет. Но лучше пусть будет, ибо дисциплинирует!
package Product::GUIButton;
# ./Product/GUIButton.pm - Абстрактный продукт. Объявляет
# интерфейс для типа объекта/продукта.
sub new { return bless {} }
# интерфейсный метод - заглушка
sub paint { die "Abstract Class\n" }
1;
Для наглядности, приведу иерархию пакетов относительно каталога. Можно конечно же запихнуть всё в один файл, но мне кажется, что так красивее.
./Factory/XFactory.pm
./Factory/GUIFactory.pm
./Factory/WinFactory.pm
./Product/WinButton.pm
./Product/GUIButton.pm
./Product/XButton.pm
./Application.pm
./test_factory.pl
В виде UML диаграммы это выглядит как-то вот так.
UML диаграмма Абстрактной фабрики |
Плюсы паттерна
- Изолирует конкретные классы. Всё взаимодействие проходит через абстрактные интерфейсы. Основное приложение даже не знает имен классов конкретных фабрик и продуктов.
- Упрощает замену семейств продуктов. Приложение может изменить свою конфигурация просто подставив другую конкретную фабрику.
- Гарантированна сочетаемость продуктов. Если продукты конкретного семейства заточены под совместное использование и важно чтобы в определённый момент времени работа проходила только с объектами определённого семейства, то это без труда реализуется Абстрактной фабрикой.
- Трудно поддерживать новый вид продуктов. Для внедрения нового продукта придётся править всю иерархию классов начиная от Абстрактной фабрики
... вот, собственно и всё
Написано по материалам статьи в Википедии Абстрактная фабрика (шаблон проектирования) и книги Э.Гамма "Приёмы объектно-ориентированного проектирования"
ps: буду признателен, если кто-нибудь подкинет пример для perl и web разработки, где действительно полезно применять паттерн "Абстрактная фабрика".
Подробно и понятно. Спасибо!
ОтветитьУдалитьИзвините, но тут имхо ошибка.
ОтветитьУдалитьВ классе Абстрактной фабрики нельзя инкапсулировать логику выбора самих фабрик!
То есть метод sub "create {}" надо убрать из Factory::GUIFactory. А логику выбора фабрики поместить в само Application.