Выпуск 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 — Революция →