Pull to refresh

Moose(X). Продолжение

Reading time6 min
Views2.5K
В продолжение топика Use Moose; Современное ООП в Perl, я хочу рассказать про некоторые интересные возможности, которые предоставляет Moose и его расширения.
Из упомянутого выше топика можно понять, что с Moose вам не придется тратить время на реализацию объектной модели. Но кроме автоматического создания аксесоров/мутаторов и конструктора, есть еще куча всяких полезностей и интересностей. А уж с помощью расширений, так вообще можно преобразить Perl до неузнаваемости (в хорошую, естественно, сторону).

Примеры кода я буду приводить из своего модуля, который я пробую переписать в стиле modern perl.

Часть 1. Moose



1.1 Изменение отнаследованных атрибутов

Начнем с простого. Как вы могли узнать из выше упомянутого топика, Moose-based классы могут помимо методов наследовать еще и атрибуты. А еще они могут их [отнаследованные атрибуты] частично изменять. Очень удобно. Ситуация, где это может понадобиться: есть базовый класс и несколько наследующих от него. Среди атрибутов базового класса есть такие, которые нужны всем дочерним, вот только значение по-умолчанию у них должно быть у каждого свое. Вам не придется в каждом дочернем целиком объявлять атрибут со всеми свойствами (тип данных, права доступа и т.д.)

Код:
базовый класс:
has request_type => ( is => 'ro', isa => 'Int', lazy => 1, default => undef, init_arg => undef, );

дочерний:
has '+request_type' => ( default => 32, ); # сохраняем все свойства кроме default


1.2 Модификаторы методов

В некотором роде похожи на декораторы, хотя ими не являются. Есть три модификатора: before, after и around. Певые два выполняются соответственно ДО и ПОСЛЕ метода к которому их применили. Про третий стоит рассказать чуть подробнее. Модификатор around принимает в качестве аргумента имя метода к которому его применили, и уже внутри модификатора вы сами должны явно его [метод] вызвать. Получается, что происходит внутри модификатора, причем в том месте, где вам надо. Соответственно вы можете делать что угодно как ДО вызова, так и ПОСЛЕ.

Пример применения: (модификатор шифрует некий запрос после его создания, если указаны соответствующие опции)
after create_request => sub {
    my $self = shift;

    return unless $self->cipher;

    require Crypt::TripleDES;

    my $ciphered_text = sprintf "Phone%s\n%s",
        $self->trm_id,
        unpack('H*', Crypt::TripleDES->new->encrypt3($self->request, $self->ciphering_key));

    $self->request($ciphered_text)
};


1.3 augment+inner

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

Сразу код:
базовый класс:
method create_request() { # здесь создается общая часть запроса
    my $req_node = inner();
    $req_node->appendChild( $self->_create_extra_node($_, $self->$_) ) foreach qw(password serial);
    $req_node->appendChild( $self->_create_simple_node('protocol-version', $self->protocol_version) );
    $req_node->appendChild( $self->_create_simple_node('terminal-id', $self->trm_id) );
    $req_node->appendChild( $self->_create_simple_node('request-type', $self->request_type) );
    
    my $xml = XML::LibXML::Document->new('1.0', 'utf-8');
    $xml->setDocumentElement($req_node);
    
    $self->request($xml->toString)
}

дочерний:
augment create_request => sub { # а здесь только специфическая
    my $self = shift;

    my $xml = $self->_create_simple_node('request');
    $xml->appendChild( $self->_create_extra_node('phone', $self->phone) );

    $xml
};

Как это работает: в методе create_request (кстати, обратите внимание на его объявление. Об этом во 2ой части) есть вызов «магической» функции inner(). Это и есть та часть, которая может быть изменена. В дочернем классе мы передаем анонимный саб модификатору augment. Вот он-то и выполнится на месте inner() при вызове create_request у экземпляра дочернего класса.
На первый взгляд, кажется мудрено и бесполезно. Мудрено — да, бесполезно — нет. Для облегчения понимания стоит провести аналогию с изменением наследованных атрибутов: дочерний наследует метод, но частично сохраняет родительский функционал.
Только не стоит злоупотреблять этой возможностью, т.к. она снижает «прозрачность» кода.

Часть 2. MooseX


Здесь начинаются настоящие интересности) Кстати, на всякий случай скажу, что MooseX — пространство имен модулей для расширений Moose.

2.1 MooseX::Declare

Модуль предоставляет отличный синтаксический сахар для Moose-based классов. С ним методы становятся методами, а классы — классами:
use MooseX::Declare;

class Business::Qiwi {
    has trm_id => ( is => 'rw', isa => 'Str', required => 1, );
    has password => ( is => 'rw', isa => 'Str', required => 1, );
    has serial => ( is => 'rw', isa => 'Str', required => 1, );
#...
    method get_balance() {
# ...
    }
};

А можно и вот так:
class Business::Qiwi::Balance extends Business::Qiwi::Request {
# ...
};

Еще пара важных полезностей:
  1. больше не нужно делать my $self = shift;. Инвокант доступен в методе автоматически (под именем $self).
  2. классы объявляенные при помощи MooseX::Declare автоматически становятся неизменяемыми (immutable). Т.е. вам больше не нужно делать __PACKAGE__->meta->make_immutable


2.2 MooseX::Types

Создание новых типов данных (в т.ч. на основе уже имеющихся). Пример:
use MooseX::Types -declare => [qw(Date EntriesList IdsList TxnsList BillsList)]; # объявляем собственные типы
use MooseX::Types::Moose qw(Int Str ArrayRef HashRef); # импортируем стандартные типы Moose

subtype Date => as Str => where { /^\d{2}\.\d{2}\.\d{4}$/ } => message { 'Date must be provided in DD.MM.YYYY format' }; # создаем свой тип на основе стандартного Str

С использованием MooseX::Types можно не брать в кавычки названия типов данных:
subtype IdsList => as ArrayRef[Int]; # тоже самое действует для has и прототипов методов


2.3 MooseX::Declare + MooseX::Types

Если использовать два выше упомянутых модуля, то можно делать вот так:
method create_bill(Num $amount, Str $to, Str $txn, Str $comment, Bool $sms_notify?, Bool $call_notify?, Int $confirm_time?) { # присутствует синтаксис для указания именновых/позиционных/необязательных аргументов
# ...
}

method get_bill_status(BillsList $bill) { # можно использовать собственные типы данных!
# ...
}


2.4 MooseX::MultiMethods

Добавляет возможность перегрузки (overloading) методов с помощью ключевого слова multi. За меня прекрасно скажет вот этот кусок кода из тестов к модулю (привет, «Теория Большого Взрыва»):

use strict;
use warnings;
use Test::More tests => 3;

{
    package Paper; use Moose;
    package Scissors; use Moose;
    package Rock; use Moose;
    package Lizard; use Moose;
    package Spock; use Moose;

    package Game;
    use Moose;
    use MooseX::MultiMethod;

    multi method play (Paper $x, Rock $y) { 1 }
    multi method play (Paper $x, Spock $y) { 1 }
    multi method play (Scissors $x, Paper $y) { 1 }
    multi method play (Scissors $x, Lizard $y) { 1 }
    multi method play (Rock $x, Scissors $y) { 1 }
    multi method play (Rock $x, Lizard $y) { 1 }
    multi method play (Lizard $x, Paper $y) { 1 }
    multi method play (Lizard $x, Spock $y) { 1 }
    multi method play (Spock $x, Rock $y) { 1 }
    multi method play (Spock $x, Scissors $y) { 1 }
    multi method play (Any $x, Any $y) { 0 }
}

my $game = Game->new;
ok($game->play(Spock->new, Scissors->new), 'Spock smashes Scissors');
ok(!$game->play(Lizard->new, Rock->new), 'Rock crushes Lizard');
ok(!$game->play(Spock->new, Paper->new), 'Paper disproves Spock');

1;


Это был короткий обзор фишечек Moose (и его расширений) — новой объектной модели Perl5.
В светлом будущем, возможно, я попробую рассказать про роли в качестве замены наследованию. За сим позвольте откланяться.
Tags:
Hubs:
+17
Comments37

Articles

Change theme settings