суббота, 13 ноября 2010 г.

Паттерны проектирования. Perl. Абстрактная фабрика.

Абстрактная фабрика представляет собой интерфейс для создания взаимосвязанных или взаимозависимых объектов, не специфицируя их конкретных классов.

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

Основной скрипт. Тут и намёка нет на Абстрактную фабрику и нашу усложнённую реализацию. При скриптовом подходе, вероятнее всего тут же была бы проверка на тип операционки. Мы пойдём более элегантным путём.

#!/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 разработки, где действительно полезно применять паттерн "Абстрактная фабрика".

    2 комментария:

    1. Подробно и понятно. Спасибо!

      ОтветитьУдалить
    2. Извините, но тут имхо ошибка.
      В классе Абстрактной фабрики нельзя инкапсулировать логику выбора самих фабрик!
      То есть метод sub "create {}" надо убрать из Factory::GUIFactory. А логику выбора фабрики поместить в само Application.

      ОтветитьУдалить