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

    В продолжение топика 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.
    В светлом будущем, возможно, я попробую рассказать про роли в качестве замены наследованию. За сим позвольте откланяться.
    Поделиться публикацией
    Комментарии 37
      0
      Хм, вроде начинает нравиться, надо будет как-нибудь опробовать на чем-нибудь мелком.
      Вижу пока один недостаток — отсутствие подсветки синтаксиса в редакторах.
      0
      Для Moose есть стандартный errorHandler? Хочу избежать засорения кода лишними обертками из eval BLOCK.

      Должно получиться нечто похожее на:
      my $game = Game->new or die Game->errstr;
        +1
        В «комплекте по-умолчанию» такого нет. Хотя вобщем-то и среди расширений я такого не видел.
        Но реализовать такое вручную не сложно: вы можете вмешаться в работу стандартного конструктора используя ф-ю BUILD.
        package Foo;
        use Moose;

        has errstr => ( is=>'ro', isa => 'Str', lazy => 1, ); # правильнее будет вынести это в роль, но для сокращения кода оставлю так

        sub BUILD {
        my $self = shift;
        my $obj = eval { $self->SUPER::BUILD(@_) };
        $self->errstr($@) if $@;

        $obj
        }


        PS: код не проверял, писал сразу сюда
          0
          Замечательно. Теперь понятно куда копать. Благодарю за BUILD.
            0
            «The BUILD method is called after the object is constructed, but before it is returned to the caller. The BUILD method provides an opportunity to check the object state as a whole. This is a good place to put logic that cannot be expressed as a type constraint on a single attribute.»

            search.cpan.org/dist/Moose/lib/Moose/Cookbook/Basics/Recipe10.pod
              0
              Я тоже читал документацию. Что я должен из этого извлечь?
                0
                В примере ошибка. Он просто банально не сработает. Так как объект УЖЕ сконструирован.
                  0
                  Пример всего-лишь указывал, что можно посмотреть в сторону BUILD. Код я написал, что называется «от балды». С SUPER, пожалуй, получилось и правда не очень удачно, т.к. кажется как-будто я пытаюсь вернуть другой объект. Можно чуть подправить и сделать вот так (в итоге получается почти то же самое):
                  sub BUILD {
                    my $self = shift;
                    # some checks
                    unless($success) {
                       $self->errstr('bla')
                    }
                  }
                  # ...
                  my $foo = Foo->new;
                  die $foo->errstr if $foo->has_errstr;
                  
              0
              По ошибкам — сейчас активно используется модуль нашего британского коллеги TryCatch — search.cpan.org/~ash/TryCatch-1.001001/lib/TryCatch.pm
              0
              Хотелось бы еще добавить одну строчку
              __PACKAGE__->meta->make_immutable;

              search.cpan.org/~drolsky/Moose-0.79/lib/Moose/Cookbook/Basics/Recipe7.pod

              В большинстве случаев немного ускоряет работу. Как пишут сами девелоперы:
              «We strongly recommend you make your classes immutable. It makes your code much faster, with a small compile-time cost. This will be especially noticeable when creating many object.»
                +2
                Еще можно делать no Moose перед этим, что в скриптах использующих ваши классы не был доступен синактический сахар. Вот только к «фишкам» это сложно отнести.

                ЗЫ: кстати, при использовании MooseX::Declare это делается автоматически. Об этом написано в конце п. 2.1
                +2
                когда перл перестаёт быть перлом, и начинается изучение «нового языка»…
                вообщем я против такого. если нужна вся полнота ООП — добро пожаловать в яву. а то что над перлом создаётся такая прослойка — это не рентабельно для бизнеса ни в качестве поддержки, ни в качестве расширения.
                  +2
                  А люди вот думают (а главное и делают) по другому. В качестве примера вам — BBC Corp. У них внутри довольно активно используется перл, и сейчас его рефакторят с использованием Moose.
                  Тем более, что перл конечно же остается перлом. Вся его гибкость и свобода, которую он предоставляет разработчику, остается.
                    +5
                    > если нужна вся полнота ООП — добро пожаловать в яву

                    «Полное» ООП в яве? Бред, там сроду не было полного ООП.

                    Полное ООП есть в Smalltalk, Newsqueak, etc.
                      +1
                      я говорил про нормальные языки, а не про академических заморышей…
                        0
                        Что, Smalltalk-то академический? :)

                        Newsqueak — да, академический, но его потомки — нет.
                    • НЛО прилетело и опубликовало эту надпись здесь
                        0
                        тут был пост, что перл-программисты самые счастливые :)
                        –1
                        Тогда ваш язык — PHP. Перл дает богатое пространство выбора, которым не всякий способен воспользоваться. Но Перл — язык для лушчих :)
                          0
                          мой язык — перл. и при этом я рад, что я с вами не работаю над одним проектом. в промышленном программировании вот это «богатое пространство выбора» в итоге приводит к тому, что на определённом этапе быстроразвивающегося проекта поддержка превращается в непрекращающийся аврал.
                        +1
                        О! хорошая статья, спасибо. Modern Perl5 и Perl6 внешне уже похожи на диалекты одного и того же языка, в отличии от просто Perl5 и Perl6 :)
                          0
                          По поводу augment+inner. А зачем собсно такие сложности? Что мешает сделать это на обычном ООП, без введения «синтаксического мусора» (и сопутствующего обслуживающего кода в Moose), просто и понятно?

                          sub create_request { # это наследуется
                          my $self = shift;
                          my $xml = $self->_get_request_xml;
                          $xml->appendChild(....);
                          ....
                          }

                          sub _getRequest_xml { # а это переопределяется в дочернем классе
                          .....
                          return $xml
                          }


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

                            На мой взгляд, это просто прослойка, которая поможет многим справиться с Perl6. Не хотите? Да пожалуйста, не надо. Можно писать очень стройные вещи используя и просто Moose. Но мне нравится, да)
                              0
                              Пока не вижу, как можно получить дублирование кода даже для сложных случаев. Вроде все решается по той же схеме, нет?

                              Мне кажется, Perl6 это все-таки другой язык, имеющий мало общего с Perl5 (кроме названия), и справляться с ним имеет смысл именно исходя из этих оснований. Не будете же Вы переопределять оператор «точка», чтобы получить в перле жаба-синтаксис для вызова методов и таким образом «справиться с Java» :)
                                +2
                                Пример получается довольно надуманный, но к сожалению в голову больше ничего не пришло: класс-«внук» не сможет сохранить часть функционала от дочернего класса первого уровня. Вы можете только целиком переопределить метод. А выделять для каждого нового уровня наследования отдельный метод — не есть хорошо. Хотя наличие таких проблем возможно является символом того, что в проектировании имеются недостатки.
                                Не поймите меня неправильно, я ведь не агитирую использование этого приема. Я рассказал, что так можно делать, и что мне удалось найти место, где (на мой взгляд) это оказалось уместно.

                                Сравнение с джавой, имхо, некорректно. Да, Perl6 сильно отличается от 5, но все-таки он не является полностью новым языком. Как ни крути, а я уверен, что многие мигруют на 6 после релиза. Даже разработчики агитируют к этому. Поэтому делать прослойку из Perl5 в Perl6 имеет смысл, а вот из Perl в Java — нет.
                                Когда я увидел первые наброски синтаксиса Perl6 (особенно гипер-операторы), я был уверен, что я не буду его использовать. Сейчас я уже не так уверен. Конечно, legacy-код никуда не денется, но для новых проектов я возможно буду использовать 6 версию.
                                  0
                                  Спасибо, пример понятен. Во многих случаях эта проблема действительно отпадет на этапе проектирования, но не всегда. А что касается 6ки, мне проще считать что это совсем отдельный язык, нежели продолжение 5й ветки. Уж больно велики различия. Вообще конечно поскорее бы они уже ее выпустили, ждать устали все :)
                              0
                              Перл это язык, на котором можно легко написать Перл 6 и Перл 7.
                              А для тех старпёров, кто боится вылезти за Perl::Core на perl.org давно построили отдельное кладбище.
                                0
                                Написать-то легко. Вопрос в том, зачем? Чтоб былО? Или чтоб быстрее работало?
                                  –1
                                  Ответ на свой вопрос попробуйте найти самостоятельно :)) Это как разговаривать слепому с глухим.
                                0
                                Кстати, что супер круто- так это то, что таким образом выкристаллизовываются оптимальные практики программирования. И нет никаких правил законов и ограничений — можно построить хоть марсоход. А вот Перл6 может получиться удачным, а может и нет — и если Perl-Core будет неудачным, то это поставит крест на Перл6. Но Перл 6 и Муз очень положительно друг на друга влияют, так что за ближайший год Муз может определить дальнейшее развитие Перл 6 и его базовое наполнение.

                                А по поводу накладных расходов- на них больше потрачено энергии на разговоры в ваттах, чем ватт электричества на эти самые накладные раходы.
                                0
                                Перл действительно мегаязык, хоть и уже 4 года на нем не пишу (за исключением консольных скриптов).

                                Кто-нибудь знает, как технически делается подобное? Пришел в шок, когда это увидел:

                                class Business::Qiwi::Balance extends Business::Qiwi::Request

                                Каким образом так расширяемся синтаксис?
                              • НЛО прилетело и опубликовало эту надпись здесь
                                  0
                                  Пиши еще!

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое