Выпуск 22. Декабрь 2014
← Анонс воркшопа Saint Perl 2014 | Содержание | Perl 6 XXI века →ООП. Основные паттерны проектирования. Реализация в Perl
Материал статьи для уровня Beginners. Здесь не будет Moose, только чистый Perl. Предполагается, что какое-то ООП в Perl уже знакомо
Паттерны это стандартные приемы, решающие небольшую конкретную задачу. Это не инструкция, как писать код, а схема или принцип организации кода, модулей и т. п. Уверена, что если вы их не знаете на уровне диаграмм UML, то встречали в коде. Этот небольшой обзор познакомит с самыми простыми, полезными и часто используемыми паттернами.
Singleton (Одиночка)
Порождающий паттерн. Используется в случае, когда в системе должен быть только один экземпляр какого-то класса. Например, подключение к базе, распарсенный файл конфигурации и т. д. Но при этом вы не хотите таскать с собой какие-то глобальные переменные. Невероятно удобен для отложенных инициализаций тех же конфигов.
Реализация
Пусть у нас будет какой-то абстрактный класс с именем MyClass
.
package MyClass;
use strict;
our $singleton = undef;
sub new {
my $class = shift;
return $singleton if defined $singleton;
my $self = {};
$singleton = bless($self, $class);
$singleton->init();
return $singleton;
}
# other methods
sub init {
#...
}
1;
$singleton->init();
— вот тут, к примеру, проводится какая-то инициализация (либо она может быть отложена до вызова конкретных функций).
Пример использования
use MyClass;
use strict;
sub f {
print MyClass->new()->{name}, "\n";
}
sub f2 {
print MyClass->new()->{name}, "\n";
}
my $obj = MyClass->new();
$obj->{name} = 'Bob'; # это не ООП!
f();
f2();
$obj->{name} = 'Mike'; # и это тоже
f();
f2();
На выходе
Bob
Bob
Mike
Mike
В результате вызова функций f()
и f2()
мы получим один и тот же созданный объект, ссылка на который хранится у нас в $MyClass::singleton
, с ней можно работать напрямую, но это моветон и делать так не надо (за исключением ситуаций, когда требуется высокая производительность, а использование аксессоров создаёт ощутимые накладные расходы).
Таким образом, можно в любом месте кода создавать объект через конструктор и не волноваться, что он каждый раз будет создаваться заново.
На CPAN, кстати, есть Class::Singleton
, MooseX::Singleton
, Apache::Singleton
и еще куча других.
Abstract Factory (Абстрактная фабрика)
Порождающий паттерн. Берет на себя ответственность за создание объекта нужного класса. Мы просто обращаемся к ее конструктору, а какой нам вернуть объект, фабрика решает сама. Создаваемые объекты, конечно, должны быть из одного семейства и иметь идентичный интерфейс. То есть, они должны быть взаимозаменяемыми.
В качестве примеров использования: в номере 21 в статье Тестирование в Perl. Практика паттерн использован для создания объекта-логгера в зависимости от способа вывода: либо stderr, либо file. В более бизнесовом мире встречаются разные способы доставки (там все одинаковое, но разные формочки, разные коэффициенты какие-нибудь), разные форматы прайсов от поставщиков (у кого-то Excel, у кого-то XML), разные способы отправки уведомлений (e-mail, SMS).
У меня будет пример очень абстрактный, но очень понятный. Допустим, у нас есть ферма с животными. Нам, с точки зрения логики, все равно, какое животное будет создано, мы только задаем в параметрах, сколько у него ног. (В реальности значение количества ног мы получаем из внешнего конфига, а не задаем в коде).
Пример использования
use AnimalFactory;
my $animal_one = AnimalFactory->new(legs => 2);
print ref $animal_one, "\n";
my $animal_two = AnimalFactory->new(legs => 4);
print ref $animal_two, "\n";
$animal_one->walk();
$animal_two->walk();
На выходе
Chicken
Cow
Реализация
package AnimalFactory;
use Chicken;
use Cow;
sub new {
my $class = shift;
my $opt = {@_};
return Cow->new() if $opt->{legs} == 4;
return Chicken->new() if $opt->{legs} == 2;
}
1;
Тут важно понимать, что обращаясь к конструктору AnimalFactory, мы получаем объект класса вовсе не AnimalFactory, а того, который она решит создать.
Если нам понадобится класс Snake
, то мы просто добавим логику его создания в AnimalFactory
, как-нибудь так:
return Snake->new() if $opt->{legs} == 0;
Если вдруг Cow
нужно будет заменить на Horse
, это нужно будет сделать только в одном месте — в AnimalFactory
, не затрагивая других участков кода.
Абстрактную фабрику стоит использовать там, где класс объекта зависит от каких-нибудь внешних факторов: пользовательских настроек, версии браузера, ОС и т. п.
(В некоторых случаях не очень хорошо, что мы подгружаем все возможные классы сразу через use
, это можно изменить: внести внутрь конструктора и подключать классы через require
уже после анализа параметров и до создания конкретного объекта.)
Template Method (Шаблонный метод)
Паттерн поведения. Паттерн используется для определения основного алгоритма для всех подклассов. Берем алгоритм, делим его на много мелких этапов, пишем в базовом классе, а все подклассы реализуют различающиеся части.
Самый простой пример: импорт товаров от поставщика. Нужно распарсить файл, пройти по всем товарам от поставщика, если товар найден — обновить его, если не найден — создать, подсчитать конечную стоимость, записать операцию с товаром в журнал, проделать что-нибудь еще с чем-нибудь.
Использование
my $import = ImportFactory->new(type => 'Bekka');
$import->do;
(Здесь я использую фабрику для создания нужного мне объекта по имени поставщика, от которого загружается файл.)
Но можно обойтись и без фабрики, а сделать вот так (хотя гибкость это явно снижает, но она и не всегда такая нужна):
my $type = 'Bekka';
my $import = $type->new();
$import->do;
Реализация
Допустим, у меня тут два поставщика: Bekka
package Bekka;
use base 'Import';
sub parse {
# parse Excel
}
sub count_price {
# price * 2
}
1;
который присылает файлы в Excel, и у которого цену из файла нужно увеличивать в два раза.
И Pukka
, у которого файлы в XML, а цену нужно делить пополам:
package Pukka;
use base 'Import';
sub parse {
# parse XML
}
sub count_price {
# price / 2
}
1;
Оба эти класса имеют родителя Import
, который и описывает основной алгоритм загрузки файла (sub do
). В нем определяются все используемые методы, но работающие по какому-то умолчанию. (У методов, конечно, еще есть какой-нибудь код, но здесь он не нужен, поэтому его не привожу.)
package Import;
...
sub do {
my $self = shift;
$self->parse();
while ($self->next) {
if ($self->find) {
$self->update;
}
else {
$self->insert;
}
$self->count_price;
$self->log;
}
$self->finish;
}
sub next;
sub find;
sub update;
sub insert;
sub count_price {
my $self = shift;
# use original price
}
1;
Получается: фабрика создает нам объект нужного класса, основываясь на имени поставщика. Базовый объект для него описывает весь процесс импорта товара от любого поставщика. Объект конкретного класса переопределяет те методы, которые ему не подходят, на свою реализацию — в нашем случае методы count_price
и parse
.
Метод do
из класса Import
и есть наш шаблонный метод — он описывает шаблон поведения. И вовсе необязательно, что он должен его реализовывать. В реальности сложно найти задачи такого плана, которые могут быть удовлетворены поведением по умолчанию.
Удобно использовать констукцию can
для методов, которые не обязательно должны быть в базовом классе, но могут быть в подклассах: $self->do_smth if $self->can('do_smth')
, тогда метод будет вызваться только в том случае, если он реально определен. Это избавит от кучи пустого кода, а также позволяет писать довольно удобно хуки, типа:
$self->before_update() if $self->can('before_update');
$self->update();
$self->after_update() if $self->can('after_update');
Strategy (Стратегия)
Паттерн поведения. Другое название — Политика. Используется для взаимозаменяемости алгоритмов или их фрагментов. Например, когда у нас есть разные способы расчета скидки на заказ. (Пример высосан из пальца, и для таких случаев делать подобные схемы — роскошь. Но он прост и понятен.)
Использование
use DiscountFactory;
use Order;
my $order = Order->new();
$order->{summa} = 200; # так делать — не ООП! Это только для примера
my $discounter = DiscountFactory->new(type => 'Visa');
print $order->get_summa(discounter => $discounter), "\n";
$discounter = DiscountFactory->new(type => 'yandex');
print $order->get_summa(discounter => $discounter), "\n";
На выходе
196
210
Реализация
Класс Заказ
package Order;
sub new { return bless {}, shift }
sub get_summa {
my $self = shift;
my $opt = {@_};
my $summa = $opt->{discounter}->do(summa => $self->{ summa });
return $summa;
}
1;
Фабрика DiscountFactory
(ее кода здесь нет, там все как и в обычной фабрике) возвращает объекты класса либо DiscountVisa
, либо DiscountYM
:
package DiscountVisa;
sub new { return bless {}, shift }
sub do {
my $self = shift;
my $opt = {@_};
# Здесь я позволила себе использовать
# «магическое число» --- это только для наглядности
# примера. Так делать плохо.
return $opt->{summa} * (1 - 0.02);
}
package DiscountYM;
sub new { return bless {}, shift }
sub do {
my $self = shift;
my $opt = {@_};
return $opt->{summa} * (1 + 0.05);
}
1;
В классе Order
у нас есть метод get_summa
, который возвращает конечную стоимость заказа, но он должен учитывать и скидку на заказ. А скидка на заказ определяется способом оплаты заказа.
my $discounter = DiscountFactory->new(type => 'Visa')
— создали наш объект-дискаунтер, который знает, как считать скидку при оплате картой Visa.
$order->get_summa(discounter => $discounter)
— вызываем метод для получения итоговой стоимости заказа, передавая туда нашу «стратегию» расчета скидки.
my $summa = $opt->{discounter}->do(summa => $self->{ summa });
— в методе get_summa
мы вызываем операцию применения скидки к нашей базовой стоимости заказа.
То есть, мы передаем стратегию расчета скидки для заказа в качестве параметра. Эту же стратегию мы можем в дальнейшем использовать и в других функциях, работающих со стоимостью заказа, заменять ее, не меняя остальной код.
На деле все очень просто, в следующей статье обязательно расскажу про другие очень используемые паттерны с чуть более сложной реализацией.
← Анонс воркшопа Saint Perl 2014 | Содержание | Perl 6 XXI века →