Perlでプラガブルモジュールを作ろう!

第4回Hook処理を極めて外部からモジュールを拡張する

今回のテーマ

最終回の今回は、前回までのディープな話題とはうって変わって、CPANへのモジュール登録数が世界第2位のmiyagawaさんが作成されたClass::Triggerや、Perl界隈のみならず様々な場所で話題になっているPlaggerにて実装されているHook処理にフォーカスを当てた話題を扱ってから、この連載を〆させていただこうかと思います。

サンプルアプリケーション

本連載では、プラガブルなモジュールを作製するという事を考えて、Gopperという実際に実行可能なサンプルアプリケーションを元に解説を行ないます。 GopperはCodeRepos上のsvnリポジトリに置いてあるので各自checkoutしてください。

svn co -r 1641 http://svn.coderepos.org/share/lang/perl/Gopper/trunk Gopper

Hook処理とは?

モジュール本体のコードを書き換えることなく、外部から元のモジュールの処理を拡張する事を可能にするという、 プラガブルなアプリケーションを書くための手法の一つです。

直感的に理解できるように、現在CPANに登録されているモジュールを紹介しつつ解説します。

Class::Trigger

Hook処理を実装する事が出来る代表的なCPANモジュールといえばClass::Triggerでしょう。
Class::TriggerはSledgeを拡張するために使われている事で有名です。

Class::Triggerは、とてもシンプルに実装されており、trigger pointに対して実行するコードを設定する事と、trigger pointに登録されているコードを呼び出すという2種類の処理を記述するだけで、簡単に外部から拡張可能なモジュールを作成できます。

下記のようなモジュールをあらかじめ用意しておき

# Hello.pm
package Hello;
use strict;
use warnings;
use Class::Trigger;

sub say {
    __PACKAGE__->call_trigger('say');
}
1;

# hello.pl
use strict;
use warnings;
use Hello;

Hello->add_trigger( say => sub { print 'こんにちわ!' } );

Hello->say;
上記のようなコードを実行すると、こんにちわ!と表示する事が出来ます。

add_triggerメソッドでtrigger pointと、そのtrigger pointが呼び出された時に実行するコードをセットし、call_triggerで指定されたtrigger pointに対応するコードを実行します。

add_triggerは、同じフック名でのコードリファレンス登録を複数回行うことが出来ます。

# hello.pl
use strict;
use warnings;
use Hello;

Hello->add_trigger( say => sub { print 'こんにちわ!' } );
Hello->add_trigger( say => sub { print 'こんにちわ!' } );

Hello->say;

このように、同じ登録を2回行ったとしてもこんにちわ!こんにちわ!と表示する事が出来ます。

モジュール書くけど、とりあえず簡単に拡張出来るようにしたい!といったケースにClass::Triggerを使うと、とても楽に開発を行えます。もちろん簡単な事しか出来ないわけでは無いので複雑な物にも対応出来ますよ。

既存の複雑なプログラムをClass::Triggerを使ってプラガブルにする。という内容でClass::Trigger作者のmiyagawaさんが今年のYAPC::Asia 2007 Tokyoにて発表しており、その資料が公開されていますので興味がある方は是非ご覧ください。

Plagger

筆者がClass::Componentを作る動機となったのは、Plaggerのhook処理を他のモジュールで使いたい!という欲望が根底にありました。 言ってみればPlaggerはClass::Componentのお母さんのようなものです。CatalystとDBIx::Classは、それぞれお父さんになりますでしょうか。

PlaggerもClass::Triggerの作者と同じ人が作っているだけあって、Class::Triggerと同じような実装になっています。 Hookに関連する処理の大部分はPlagger.pm中に実装されており、メソッドを表1にて抜き出しました。

表1 Plaggerで実装されているHook関連のメソッド
メソッド名役割
register_hookhook pointを登録します
run_hook指定したhook pointを実行します
run_hook_once指定したhook pointを実行し、順番にhook pointのコードを処理し、最初に戻り値があった時点で以降に登録されたコードの実行は行ないません

Hook関連のメソッドは少なく、シンプルですね。

register_hookは主にPluginから利用されるメソッドになっており、hook pointとhook poinに対するコードをPlaggerに対して登録します。
Class::Triggerで言う所のadd_triggerになります。
実際のプラグインのコードを引用すると下記のように利用されます。

# Plagger::Plugin::Publish::CHTML
sub register {
    my($self, $context) = @_;
    $context->register_hook(
        $self,
        'publish.feed' => \&feed,
        'publish.finalize' => \&finalize,
    );

    $self->chtml_init($context);
}

run_hookとrun_hook_onceはClass::Triggerのcall_triggerに相当し、指定されたhook pointに対してregister_hookにて登録されたコードを実行します。

# in Plagger->do_run_with_feedsB
$self->run_hook('publish.finalize');

もし先ほどの Publish::CHTML プラグインを使っている場合には、hook point として publish.finalizeとPlagger::Plugin::Publish::CHTMLのfinalizeメソッドが関連づけられているので、上記のrun_hookを実行した場合にはfinalizeメソッドが実行される事になります。

Class::ComponentでHookを使う

ここまでは、Class::ComponentのHookを実装するために参考にしたCPANモジュールを紹介してきました。

Class::ComponentもClass::TriggerやPlaggerと同じ考え方でHookを実装していく事ができます。

Pluginでのhook point登録

Class::ComponentでもClass::Triggerのadd_triggerに該当するregister_hookというメソッドが用意されているのですが、基本的にはregister_hookを使うことなくhook pointの登録ができます。

前々回、前回と解説してきたClass::ComponentでのAttributeとしてAttribute::Hookという物が同梱されているので、hook pointを指定したいメソッドに対してHook attributeを指定します。

# in Gopper::Plugin::Protocol::Gopher
sub request_read :Hook('request.read') {
    my($self, $c, $stash) = @_;

    my $line = $stash->engine->get_line( $stash->engine_conn->{handle} );
    $c->log(debug => "request: $line");
    $stash->request->request_line($line);
    $self->RC_OK;
}

この例では、request_readメソッドに対してrequest.readというhook pointを設定しています。

Class::Componentでの実装は、Class::Triggerのようにコードリファレンスをhook pointに登録するのでは無く、メソッド名のみを登録する為、下記のコードは動作しません。

my $request_read = sub : Hook('request.read') {

hook pointを実行する

登録されたhook pointを実行するにはrun_hookメソッドを使用します。

# in Gopper
sub run {
    my $self = shift;

    $self->log(debug => "engine.preper");
    $self->run_hook('engine.preper');
    $self->engine->run($self); # while                                                                                                                           
}

この例ではengine.preperというhook pointが実行されます。

run_hookは、指定したhook pointに登録されたメソッドを全て実行して、それら実行したメソッドの戻り値をARRAYリファレンスとして返します。


my $results = $self->run_hook('foo');
for my $result (@{ $results }) {
    ....
}

このような形で、それぞれのPluginのhook pointの戻り値を処理できます。

現在の問題点

現在のrun_hookは、あまりにも汎用的な実装になっていて、実際run_hookをそのまま使えるかどうかが微妙な物になってしまっています。

Gopperでも

# in Gopper
sub run_request_hook {
    my($self, $hook, $stash, $code_checker) = @_;
    return RC_OK unless my $hooks = 
        $self->class_component_hooks->{$hook};

    for my $obj (@{ $hooks }) {
        my($plugin, $method) = 
            ($obj->{plugin}, $obj->{method});
        my $code = $plugin->$method($self, $stash);
        return $code || RC_BAD_REQUEST
            unless $code_checker->($code);
    }
    return RC_OK;
}

のような形で独自のrun_hookを実装して利用しています。

class_component_hooksメソッドからHookに登録されているデータが全て取れるので、run_hookを独自で実装するためにはclass_component_hooksからデータを取得して処理する必要があります。

毎回標準のrun_hookを使わないと言うのも勿体ない話なので、もう少し柔軟にrun_hookを拡張する仕組みをClass::Component本体に採り入れたいと思っています。

おまけ

本連載では紹介しなかったプラガブルモジュールを軽く紹介します。

Module::Pluggable

プラガブルモジュールを作る時の定番モジュールのModule::Pluggableは、特定の名前空間以下にあるモジュールを全てリストアップし、必要であればrequireまで自動で行ってくれます。

簡単なコードサンプルを記述すると、まずメインとなるパッケージを書きます。

package MyPlug;
use strict;
use warnings;
use Module::Pluggable require => 1;
for my $plugin (__PACKAGE__->plugins) {
    $plugin->say;
}
1;
# 簡易な例として書いてるので大分乱暴なコードになっています。

その後、そのメインとなるパッケージのパッケージ名::Plugin::の名前空間以下にプラグインを書きます。 Pluginという所は別途変更も出来ます。

package MyPlug::Plugin::EN;
use strict;
use warnings;
sub say { print "hello\n" }
1;
package MyPlug::Plugin::JP;
use strict;
use warnings;
sub say { print "こんにちわ\n" }
1;

この例のコードを実際に動かしてみます。

$ perl -MMyPlug -e ''
こんにちわ
hello

しっかりとPlugin以下のモジュールをrequireして実行できていますね。

Hook::Modular

Hook::Modularは、PODにも記載の有るとおりPlaggerがらFeedアグリゲータ的な部分を削除して、Plaggerのような何かを作り出すことに最適化したフレームワークとなっています。

Class::Hooakble

Class::HooakbleはCPAN上には無くCodeRepos上で開発が進められている、PlaggerのHook処理のみを抜き出したHookに特化したモジュールです。

最後に

全4回とPerlでのプラガブルなモジュールを作るための役立つエッセンスを紹介してきました。 Class::Componentや本連載上で取り上げたモジュール以外にもプラガブルなモジュールがCPANには沢山転がっているので、それらのソースコードを読んだりすると新しい発見も発見もあるでしょう。 本連載はそれらの他のモジュールの実装を読み解くヒントとして活用されればばいいなと思っています。

もし、まだプラガブルなコードを書いていないとしたら、今まで紹介したモジュールを活用して何かしらコードを作ってみましょう!プラガブルという題材だけでも結構置くが深いものですよ。

とはいえ私はプラガブルアプリよりも、Template::Declareのような変態的テンプレートエンジンを書くのに夢中になってますが。

Enjoy!

おすすめ記事

記事・ニュース一覧