четверг, 20 августа 2009 г.

Паттерн Adapter

Да, собрался с силами и нашел время, чтобы написать, а скорее самому разобраться, с этим паттерном.

Adapter

Применяется (кратко):
  • изменение интерфейса существующего класса
  • объединение интерфейсов нескольких классов
За более подробной информацией идем сюда. Мне интересна только реализация с использование Perl.

Задача:
Что есть: реализуется простой, быстрый web-framework.
Что требуется: возможность быстрой замены шаблонизатора.
По умолчанию используем HTML::Template::Pro, но в будующем надеемся перейти на HTML::CTPP2, либо что-нибудь еще более быстрое ))

К сожалению в Perl нет интерфейсов, хотя можно сделать, что то вроде абстрактного класса (зачем?? :) ). Из этого следует что Class Adapter нам точно не подходит. Поэтому быстро и просто сделать Object Adapter (лучше всегда двигаться по пути наименьшего сопротивления).

Что получили (адаптируем HTML::Template::Pro):


package HTMLTemplateProTemplate;
use strict;

use HTML::Template::Pro;
use File::Spec;

sub new
{
my $class = shift;

return bless {
tmpl => new HTML::Template::Pro(filename => ''),
cache => {},
}, $class;
}

sub set_path
{
my ($self, $path) = @_;

$self->{tmpl}->{path} = $path;
}

sub get
{
my ($self, $tmplname) = @_;

return undef unless $self->_find_template($tmplname);

$self->{tmpl}->{filename} = $tmplname;

return $self;
}

sub _find_template
{
my ($self, $tmplname) = @_;

return $self->{cache}{$tmplname}
if defined $self->{cache}{$tmplname};

for my $dir ( @{$self->{tmpl}->{path}} ){
my $fname = File::Spec->catfile($dir, $tmplname);
if (-e $fname){
$self->{cache}{$tmplname} = $fname;
return $fname;
}
}
return undef;
}

1;


Клиет для этого класса:


package Template;
use HTMLTemplateProTemplate;
use Moose;

has path => ( is => 'ro', default => sub { [] } );
has engine => ( is => 'ro' );

sub new
{
my $class = shift;

my $self = bless { @_ }, $class;

$self->_init;

return $self;
}

sub _init
{
my $self = shift;

$self->{engine} = new HTMLTemplateProTemplate()
unless $self->{engine};

$self->{engine}->set_path( $self->path );
}

sub get
{
my ( $self, $tmplname) = @_;

$self->{engine}->get($tmplname);

return $self->{engine};
}

sub render
{
my ( $self, $context ) = shift;

return $self->{tmpl}->render($context);
}
1;




Когда мы решим использовать HTML::CTPP2 мы адаптируем его для нашего клиента и будем счастливы.

пятница, 24 апреля 2009 г.

Singlton and Monostate


Singlton
Позволяет создавать один экземпляр класса, и использовать его на протяжении всей жизни процесса.

Обычная мотивация применения паттерна* :
  1. Необходимость - иметь один объект выполняющий однотипные действия (например Logger, Config)
  2. Требование - должен быть один обьект, обычно контролирующий/обрабатывающий запросы (например Module)

Monostate
Дает такое же поведение что и синглтон, но не требует единичного экземпляра.
"Не один экземпляр, а одно состояние множества экземпляров".


Рассмотрим следующую ситуацию.
На сайт зашел пользователь. Есть два варианта 
  1. он авторизован 
  2. он не авторизован
Нам от него необходимо знать откуда он (регион). В первом случае мы можем посмотреть это в базе (т.к. всю возможную информацию о пользователе мы сохраняем), во втором случае у нас есть только ip.  Далее предположим, что мы можем запросить один из наших проектов опредилить регион пользователя по ip, партнерский проект отдает нам ID региона. У нас есть таблица соответствия ID_региона_партнера => ID_региона_в_нашей_базе. 
Зачем нам все это? У нас имеются объявления, которые необходимо показывать пользователям. Обьявления имеют свой гео-таргетинг. Таким образом, для того что бы показать обьявление, нам необходимо знать из какого региона пользователь.
Можно спросить, "Что стоит сделать небольшой запрос к БД?", что бы получить ID региона в нашей базе, по имеющемуся ID партнера. Да, проблем нет, данныйй запрос выполнится максимально быстро, стоимость этого запроса минимальна, поиск идет по уникальному индексу. Но есть одно "но", на проекте количество запросов к основному бэкенду 10-20 в секунду. И что, получается будем дергать базу 10 раз в секунду? Конечно же нет. Мы все это сдампим один раз и потом будем отдавать из памяти.

Другой пример. При использовании mod_perl, при инициализации сессии, мы получаем ссылку на объект Apache::Request. Мы можем все время пробрасывать ссылку на этот объект в вызываемые методы, а можем сохранить ее и использовать сохраненную ссылку.


Покажем как это будет выглядеть:
  • Singlton

package Region;
use strict;
use warnings;

my $_self = undef;

sub new
{
my $class = shift;

if ( ref $_self ) {
return $_self;
}

$_self = bless {}, $class;
return $_self->_init();
}

#инициализация хеша регионов
sub _init
{
my $self = shift;

$self = new($self) if !ref $self;

#инициируем хеш регионов
#обращаемся к базе за списком (либо как-нибудь еще)
my $st = $dbh->prepare("
begin
:cr := region.getRegions;
end;
");

$st->bind_param_inout(':cr', \$cr, 0, {ora_type => ORA_RSET});
$st->execute;

$st = $cr->fetchall_arrayref({}); $cr->finish; $cr = $st;

for (@$cr) {
$self->{$_->{PID}} = $_->{OID};
}

return $self;
}

#получение региона
sub get
{
my $self = shift;
my $id = shift;

$self = new($self) if !ref $self;

return $self->{$id};
}
1;


  • Monostate
package ApacheRequest;
use strict;
use warnings;

my $AR= undef;

sub new
{
        my $class = shift;

        my $self = bless [], $class;
        return $self;
}

sub set
{
        my $self = shift;
        my $requestHandle = shift;

        $AR = $requestHandle;
}

sub get
{
        my $self = shift;

        return $AR;
}

1;

Производительность обоих объектов соизмеримо равна. Так что, если вы реши использовать у себя в проекте один из этих паттрнов, то можете выбирать что больше нравится.
Я пользуюсь правилом
  • singlton - если необходимо кеширование
  • monostate - если уж очень сильно нужны глобальные объекты

Паттерны дают нам готовые решения, но от того как вы их будете использовать, зависит судьба вашего проекта. 
В примере с Monostate создается глобальная переменная, вследствии чего увеличивается зависимость пакетов, появляются накладные расходы по написанию тестов.

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

* Возможно имеются и другие обычные мотивации для использования Одиночки, но просматривая информацию в Интернете, я натыкался на перечисленные мотивации.

четверг, 23 апреля 2009 г.

Паттерн Strategy

Ох уж эти паттерны. Но весчь хорошая и нужная.

Если разобраться, то паттерны мы используем всегда, просто не знаем о том, что это уже давно описанный шаблон проектирования. Мы просто снова и снова изобретаем колесо. Зачем? Почему? А потому что не читаем книг.

Стратегия.
Что такое стратегия? Не буду перепечатывать то, что можно нагуглить, скажу просто. 
Стратегия - не детализированный способ достижения поставленной цели.

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

Как это обычно выглядит?
Предположим нам необходимо показать пользователю* блок с контекстной рекламой. При этом необходимо ротировать и таргетировать блоки по тем данным что мы знаем о пользователе, к примеру: регион, пол, возраст и т.д.
Мы реализовали несколько алгоритмов ротации: равномерная ротация, вытесняющая,  волновая и т.д.
Мы можем выбирать любой из этих алгоритмов, который мы хотим использовать в данный момент


if ($choice == ADV::EVEN) {
$blocks = &_getEvenDistribution($in_blocks);
}
elsif ($choice == ADV::UNDU) {
$blocks = &_getEjectDistribution($in_blocks);
}
..................

sub _getEvenDistribution
{
my $blocks= shift;
#нахождение нужных блоков
}

sub _getEjectDistribution
{
my $blocks= shift;

#нахождение нужных блоков
}


Что нам предлагает сделать паттерн Стратегия.
  1. Создать интерфейс (абстрактный класс) Strategy
  2. Создать конкретные стратигии (алгоритмы) 
  3. Создать контекст
  4. Все это объединить


Давайте это и сделаем:

# базовый класс стратегии
package AbstractStrategy;

sub new {
my $self= shift;

# запрещаем создавать экземпляр этого класса
die "need object" if (!ref $self);

#что то делаем, если требуется

return $self;
}

# что то вроде абстрактного метода 
sub rotate
{
my $self = shift;
die "can not invoke";
}
1;

# класс реализующий равномерную ротацию
package EvenStrategy

use base qw(AbstractStrategy);

sub new
{
my $class = shift;

my $self = bless {}, $class;

return $self->SUPER::new(@_);
}

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

# sub    rotate($blocks)
# in       $blocks - массив хешей блоков
# out     $block - самый кликабельный

sub rotate
{
my ($self, $blocks) = @_;

my ($commonWeight, $rand) = (0, 0);

#будем считать что блоки отсортированы по весу
#если нет, то это просто сделать

for (@$blocks) {
$commonWeight += $_->{weight};
$_->{interval} = $commonWeight;
}
$rand = int(rand(0, $commonWeight));
#для простоты будем искать обычным перебором, 
#если блоков много лучше применять быстрый поиск
for ($i = 0; $i < (scalar(@$blocks) -1); $i++) {
if( $rand < $blocks->[$i]{weight}) {
return $blocks->[$i];
}
}
}
1;

#класс реализующий вытесняющую ротацию
package EjectStrategy

use base qw(AbstractStrategy);

sub new
{
my $class = shift;

my $self = bless {}, $class;

return $self->SUPER::new(@_);
}

# sub    rotate($blocks)
# in       $blocks - массив хешей блоков
# out     $block - самый кликабельный
sub rotate
{
my ($self, $blocks) = @_;

#сортируем по количеству кликов
@$blocks = sort { $a->{clicks} <=> $b->{clicks} } @$blocks;

return ( pop @$blocks);
}
1;

# Класс контекста

package AdvContext;

sub new
{
my $class = shift;
my $strategy = shift;

#запретим создание экземпляра, если нам подсунули
  # обект у которого нет метода rotate
if(!UNIVESAL::can($strategy, 'rotate')) {
die "need strategy";
}

my $self =  bless {strategy => $strategy }, $class;

return $self;
}

sub getBlock
{
my $self = shift;
my $blocks = shift;

retrun $self->{strategy}->rotate($blcoks);
}
1;

#обработкичи запросов пользователей
.............
sub evenHandler
{
my $context = AdvContext->new(EvenStrategy->new());
return $context->getBlock($global->blocks);
}

sub ejectHandler
{
my $context = AdvContext->new(EjectStrategy->new());
return $context->getBlock($global->blocks);
}


Следует отметить, ввиду специфики языка программирования Perl, класс AbstractStrategy является бесполезным классом. Его описание и использование можно опустить.

Когда следует использовать паттерн стратегия:
  • имеется много родственных классов, отличающихся только поведением
  • необходимо иметь несколько разных вариантов алгоритмав алгоритме содержатся данные, о которых клиент не должен знать  паттерн стратегия позволяет не раскрывать сложные, специфичные для алгоритма структуры данных
  • в классе определено много поведений, что представлено разветвленными условными операторами

На этой ноте хочется закончить.
В следующий раз рассмотрим паттерн Template Method.


Так как основная моя деятельность связана с web-разработкой, примеры я буду брать из этой сферы.



6F8BE4F65AE4EF89E47FDFD31E564CB3