Выпуск 1. Март 2013

YAPC::Europe 2013 «Future Perl» | Содержание | Dancer2 — Революция

Moo — современный минимальный ООП-фреймворк

Объектно-ориентированное программирование в Perl 5 представлено очень базовыми вещами. Однако пробелы в реализации ООП были заполнены Perl сообществом в виде модулей на CPAN. В данной статье будет рассказано об истории появления этих реализаций и, в том числе, о Moo, как современной рекомендуемой практике.

ООПокалипсис или немного истории ООП в Perl

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

Вкратце, суть такова, что изначально Perl не был явно ориентирован на какую-либо определённую парадигму программирования. Выпуск Perl 5 предоставил программистам возможность использовать ОО-подход в разработке, создавать модули и компоновать классы. Позже однако выяснилось, что появившийся функционал был достаточно беден и заставлял людей больше думать над способом реализации, чем над самой логикой классов и объектов. Значительный редизайн ООП был запланирован для Perl 6, но эту реализацию увидят вживую разве что только наши внуки, а людям было нужно уже сейчас и в Perl 5.

Так и началось великое народное освободительное движение по расширению пространства имён Class::* и иже на CPAN. Безусловным лидером в этом восстании классов стал Moose, показав исключительно продуманную и богатую возможностями систему и став де-факто стандартом реализации ООП в Perl. Но как и у любой медали обратная сторона использования Moose — развесистые зависимости и тяжёлый взлёт приложений, написанных с его использованием. Это, в свою очередь, породило вторую волну минималистичных реализаций Moose. Так появился Mouse, который частично решал вопрос со скоростью взлёта за счёт XS-кода и несколько облегчённого функционала, и практически не имел зависимостей от других модулей.

Тяжелее всего пришлось разработчикам модулей, которым предстояло выбрать в этом море именно тот фреймворк, который можно было комфортно использовать и не навлечь на себя гнев пользователей за внезапно раздувшиеся зависимости и долгий запуск. Тут на сцену истории на белом коне въезжает Any::Moose, который обещает пользователям возможность самим решать какую Moose-реализацию использовать. Желание пользователя угадывалось по специальной переменной окружения или в зависимости от того был ли уже загружен Mouse или Moose.

Moo

В конце 2010 г. появляется новый проект минималистичной реализации объектно-ориентированного программирования для Perl с лаконичным названием Moo. Автором модуля является Matt S. Trout (mst).

Утверждается, что Moo — это аббревиатура от Minimalist Object Orientation (Минималистичная Объектная Ориентированность), хотя все прекрасно поняли, что это урезанное название ООП-фреймворка Moose. И вскоре, под тяжестью фактов, автор Moo сам сознаётся, что да, модуль является легковесным подмножеством Moose, и поскольку реализовали не все возможности, а только на две трети, то и количество букв в названии соответственно уменьшили.

Цель проекта — дать возможность разработчику начать использовать расширенный синтаксис и возможности ООП как в Moose, но при этом не иметь накладных расходов ни на время запуска, ни на объём зависимостей, а также для просты развёртывания не иметь XS-зависимостей. Более того, если программа загружает Moose, то Moo автоматически регистрирует метаклассы для ваших Moo-модулей, позволяя работать вашем коду совершенно прозрачно, как если бы он был написан с использованием Moose.

Это означает, что для Moo не требуется никакого Any::Moose — он самостоятельно сможет переключиться на Moose, в случае его загрузки. Самое интересное, что сделает это он даже лучше чем Any::Moose, поскольку для Moo совершенно не важно в каком порядке загружаются модули вашего проекта, в то время как Any::Moose при определённом порядке загрузки может сделать неверный выбор, загрузив сначала Mouse и в последствии уже не может переключиться на использование Moose, что может привести к ошибкам. Самое интересное, что в больших проектах такое может легко произойти, причём воспроизвестись в тот момент, когда вы выкатываете код в боевое окружение.

Это замечательное обстоятельство имело прямое отношение к тому, что последняя версия Any::Moose 0.20, которая вышла в последний день 2012 г. (как символично), объявляет модуль устаревшим и рекомендует всем переходить на Moo.

Good night, sweet prince…

Стоит отметить один курьёзный момент. Идея с укорачиванием имени Moose пришлась по вкусу Ingy döt Net, который выпустил yet another микро ООП-фреймворк с названием Mo. На что Matt S. Trout ответил созданием модуля M, в описании которого сообщил:

Moose привёл к созданию Mouse, приведшего к созданию Moo, приведшего к созданию Mo. Но модуль Mo Ingy так мало чего умеет, и меньше делать можно только уже абсолютно ничего не делая. Именно это и делает M — ничего.

Ingy позже выпустил модуль Moos, чтобы закрыть весь ряд букв. Кроме того, ранее у него ещё был создан модуль Mousse. Такой вот занятный CPAN-киберсквоттер.

Быстрый старт с Moo

Итак, вам уже не терпится переписать код с использованием модного и прогрессивного фреймворка. Начнём с азов. Например, попробуем создать модуль для ведения логов. Не обращайте внимание на существование Log::Dispatch или Log4perl, наш модуль будет базироваться на самом совершенном ООП-фреймворке, поэтому a priori заткнёт их за пояс.

package BestLogEver;
use Moo;
1;

Поздравляю!

Вот в общем-то и всё. На данном этапе ваш класс уже получил несколько замечательных вещей:

  • Метод new() для создания нового объекта
  • Включённая по-умолчанию прагма strict
  • Включенная по-умолчанию прагма warnings, в режиме трактующем все предупреждения как ошибки.

Напишем пример приложения, использующего этот класс:

use strict;
use warnings;
use BestLogEver;

my $log = BestLogEver->new();

У нас получилось создать объект нашего нового класса.

Теперь необходимо продумать, что будет делать наш класс. Очевидно записывать сообщения в лог. Таким образом, нашему классу потребуется метод log. Но куда записывать? На экран, в файл, в базу данных…? Вариантов может быть много и в будущем могут появляться новые, поэтому логично выделить роль Хранилища сообщений для всех видов.

package BestLogEver::Role::Storage;
use strict;
use warnings;
use Moo::Role;

requires 'log';

1;

Теперь все создаваемые в последующем хранилища будут использовать эту роль. Обратите внимание на модуль Moo::Role, который превращает наш класс в роль. Ключевое слово requires требует, чтобы класс, реализующий данную роль обязательно имел указанный метод log. Попробуем теперь переписать наш модуль так, чтобы он при создании воспринимал атрибут storage, который бы задавал какой тип хранилища мы хотим для ведения нашего лога и загружал нужный модуль.

package BestLogEver;
use Moo;

has 'storage' => (
   is => 'ro',
   isa => sub { die 'bad storage name' unless $_[0] && ! ref $_[0] }
);

1;

С помощью ключевого слова has мы объявляем атрибут storage, для которого Moo автоматически создаст getter (метод для получения значения атрибута), но не даст возможности изменения этого значения, т.к. мы объявили его как ro (read only — только для чтения). Атрибут isa задаёт функцию проверки значения нашего атрибута storage. В данном случае мы проверяем, что это не пустой скаляр. В Moo нет встроенной системы типов как в Moose, хотя это легко решается подключением MooX::Types::MooseLike. Теперь в нашем скрипте мы можем инициировать объект с атрибутом storage:

my $log = BestLogEver->new( storage => 'Screen' );
say $log->storage;

Таким образом, благодаря Moo мы уже имеем возможность проверять значение атрибута на допустимость и получить getter (и при желании setter) методы без лишних телодвижений. Попробуем написать наш первый плагин BestLogEver::Storage::Screen, выводящий сообщения на экран:

package BestLogEver::Storage::Screen;
use Moo;
with 'BestLogEver::Role::Storage';

sub log {
    my ($self, $message) = @_;
    print $message;
}

1;

Мы подключаем роль с помощью ключевого слова with. Тем самым, автоматически накладывая на модуль все требования, описанные в роли BestLogEver::Role::Storage, и в данном случае обязательное требование наличия метода log. Если наш плагин не будет иметь этого метода, то при попытке его загрузить получим ошибку:

Can't apply BestLogEver::Role::Storage to BestLogEver::Storage::Screen - missing log at /usr/share/perl5/Role/Tiny.pm line 250.

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

package BestLogEver;
use Moo;
use Module::Runtime qw(require_module);

has 'storage' => (
    is => 'ro',
    isa => sub { die 'bad storage name' unless $_[0] && ! ref $_[0] }
);

has 'storage_object' => (
    is => 'lazy'
);

sub _build_storage_object {
    my $self = shift;
    my $class = 'BestLogEver::Storage::' . $self->storage;
    require_module($class);
    return $class->new;
}

sub log {
    my ($self, $message) = @_;
    $self->storage_object->log($message);
}

1;

Поясняю по порядку.

У нас появился атрибут storage_object. Именно в нём мы будем хранить объект загруженного плагина. Атрибут объявлен как lazy, что означает две вещи: во-первых, атрибут будет создан при первом обращении, а, во-вторых, для создания атрибута будет использована функция с префиксом _build_ + имя нашего атрибута, т.е. _build_storage_object .

Далее мы реализуем функцию _build_storage_object, которая по значению атрибута storage загружает соответствующий класс и возвращает созданный объект.

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

Теперь наша программа выведет текст на экран:

my $log = BestLogEver->new( storage => 'Screen' );
$log->log("hello, world!");

hello, world!

Важно отметить, что создание атрибута storage_object происходит только один раз, во время самого первого вызова метода log.

И для ещё одной демонстрации возможностей Moo, покажем как можно модифицировать методы.

Например, нам не нравится, что при выводе сообщений не добавляется перенос строки, но и лишний перенос строки ставить не нужно, если он уже есть. Чтобы исправить это мы можем изменить поступающее сообщение в метод log у всех плагинов через класс роли Storage:

package BestLogEver::Role::Storage;
use strict;
use warnings;
use Moo::Role;

requires 'log';

before log => sub {
    chomp $_[1];
    $_[1] .= "\n";
};

1;

Ключевое слово before задаёт функцию, которая вызывается перед запуском log и может модифицировать передаваемые параметры в переменной @_, которая затем поступит в log. Код возврата из этой функции не важен.

Таким образом, мы можем «пропатчить» одним выстрелом все классы, которые выполняют указанную роль. Также вам доступна подмена самой функции (around) и изменение возвращаемых результатов (after).

Кто использует Moo

Moo определённо заслужил признание как современный ООП фреймворк в Perl. Всё больше модулей на CPAN начинают его использовать.

В качестве примеров можно привести Dancer 2 — новая версия популярнейшего фреймворка построена изначально на Moo. Известнейший ORM DBIx-Class использует Moo. Email::Sender 1.300000 вышедший в новом 2013 г. переключился с использования Moose на Moo.

Подумайте, возможны вы следующий, кто оценит модуль по достоинству и начнёт его использование.

Владимир Леттиев


YAPC::Europe 2013 «Future Perl» | Содержание | Dancer2 — Революция
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье