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

第1回Class::Componentから始めるプラガブルモジュール

はじめに

はじめまして。大沢と申します。

この連載では、筆者が実装したCPANモジュールのClass::Componentを題材にしつつ、近代的なPerlでのプラガブル(拡張可能)なソフトウェアの実装方法を紹介します。

今回は、Class::Componentの概要を説明します。

本連載で使うサンプルアプリケーション

本連載では、プラガブルなモジュールを作製するという事を考えて、Gopperというサンプルアプリケーションを元に解説を行ないます。

GopperはCodeRepos上のsvnリポジトリに置いてあるので各自checkoutしてください。

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

サンプルアプリケーションは連載中にも頻繁にupdateされる事が予想されますので、毎回リビジョン付きのsvnコマンド行を明記するようにします。

なお、Gopperを使ったサンプルは次回の連載から取りあげます。

Gopper

Gopperとは、最近再評価され始めているGopherプロトコルのサーバ実装を、Plaggerのようなプラガブルアプリケーションとして、筆者がClass::Componentを用いて実装したGopherサーバです。

サーバの各フェーズにフックポイントがあり、Apacheのモジュールを作製するノリでプラグインを実装することが出来ます。

現在はシンプルな機能しか持ち合わせていませんが、プラグインを実装することによりGopher上でCatalystSledgeなどといったWeb Frameworkを利用できたり、HTTPなどのGopher以外のプロトコルに対応する事が可能になります。

Class::Componentとは

Class::Componentとは、Perlモジュールでプラグイン機能を登載する時に必要な処理を全て引き受けてくれるモジュールです。

Class::Accessor::Fastなどのアクセサモジュールのプラガブル版だと思っていただければ間違いありません。

例えばMyClassというモジュールでプラグイン機能を使いたくなった場合には、

package MyClass;
use strict;
use warnings;
use Class::Component;
1;

とClass::Componentをuseして下さい。Class::Componentをロードする時に別途パラメータを付加する必要が無ければuse baseでも問題ありませんが、useするだけでもClass::Componentが継承ツリーに入り、機能を利用することが出来ます。

特長

Class::Componentの特長としては以下のような特徴があります。

  • Component追加で基本メソッドを拡張出来る
  • Plugin追加で、フックポイントへのフック処理追加や、Pluginメソッドを追加出来る
  • Pluginで追加するメソッドの実装方式(普通にメソッド生やす、AUTOLOAD、SingletonMethod)を選べ、選ばなければメソッド生えない
  • Plaggerのようなconfigを利用するPluginを簡単に実装できる
  • モジュール独自にAttribute処理を追加出来る
  • Class::Componentを利用したモジュールを、さらに別モジュールが継承する事が出来る
  • Class::Componentを使ったオブジェクトをYAML::DumpしてYAMLに変換した後でも、そのYAMLをYAML::Loadすれば普通に動くので、オブジェクトの永続化を取り易い

他にも特長がありますが、混み入った話になってくるので連載を通じて紹介しようと思います。

主要コンポーネント

Class::Componentは、主に3種類のコンポーネントから構成されており、それぞれComponent、Attribute、Pluginと名付けられています。

Component

作成したモジュールの基本的な動作をカスタマイズするコンポーネントになっています。

大まかな挙動としては、CatalystのPluginと似ていて、単純にモジュールの@ISAにComponentのpackageを追加して継承ツリーを太らせます。

作成するモジュールにとって、プラガブルではない拡張を施したい場合にはComponentを使って拡張を行ないます。

MyClassモジュールに対する名前空間はMyClass::Component::*になります。

Attribute

Pluginで利用するAttributeを定義するコンポーネントとなっています。

MyClassモジュールに対する名前空間はMyClass::Attribute::*となっており、MyClass::Attribute::SimpleといったAttributeを作成した場合には

package MyClass::Plugin::Foo;
use strict;
use warnings;
use base 'Class::Component::Plugin';

sub plugin_method: Simple {
    my($plugin, $context, $args) = @_;
    ...
}

1;

といった形で利用する事が出来ます。

Plugin

いわゆるプラグインで、Class::Componentを利用したモジュールの要となるコンポーネントです。

Componentと違い、プラグイン固有にインスタンスを持ち、設定もそれぞれ固有に持っています。

先ほど説明したAttributeをメソッドに対して利用する事により、モジュールを幅広くカスタマイズするプラグインを作成できます。

最小構成での実装例

Gopperは少々繁雑な処理をしているため、各要素を利用した最小限のサンプルモジュールを書きます。

MyClassモジュールを作成し、MyClassモジュールを利用するexample.plというスクリプトを書いたと想定します。

example.pl

use strict;
use warnings;
use MyClass;
my $obj = MyClass->new({
    config => {
        Hello => {
            msg => 'world'
        }
    }
});
$obj->hello; # hello world を表示
print $obj->run_hook( 'bar' )->[0]; # bog を表示
print $obj->baz; # news を表示

newしたときのオプションとしてconfigを渡しています。

configはPluginの名前をkeyにしてvalueに、そのPluginに対する設定を指定します。

MyClass

package MyClass;
use strict;
use warnings;
use Class::Component;
__PACKAGE__->load_components(qw/ Autocall::Autoload /);
__PACKAGE__->load_plugins(qw/ Hello Baz /);
1;

load_componentsにより、Class::Component::Component::Autocall::Autoloadをロードしています。

このComponentは、Pluginで拡張されるメソッドをPerlのAUTOLOADを使って、メソッドを生やすComponentになっています。

Autocall::*と名前がついているComponentは、動的にメソッドを生やす物になっていて、複数種類ある理由は、それぞれメソッドを生やす実装が異っていて、利用する方が最適な実装をチョイス出来るようにしているためです。

これが、コアの挙動を変更できるというComponentの役割を最もあらわしています。

また、load_pluginsによってMyClass::Plugin::HelloとMyClass::Plugin::Bazプラグインがロードされています。

MyClass::Attribute::News

package MyClass::Attribute::News;
use strict;
use warnings;
use base 'Class::Component::Attribute';

sub register {
    my($class, $plugin, $c, $method, $value, $code) = @_;
    no strict 'refs';
    no warnings 'redefine';
    my $cname = ref($plugin) or return;
    *{"$cname\::$method"} = sub {
        $code->(@_);
        'news';
    };
}
1;

NewsというAttributeを定義します。

このAttributeを定義されたメソッドは、どのような戻り値を指定したとしてもnewsという文字列を戻すようになります。

MyClass::Plugin::Hello

package MyClass::Plugin::Hello;
use strict;
use warnings;
use base 'Class::Component::Plugin';

sub hello :Method {
    my($self, $c, $args) = @_;
    print 'hello ' . $self->config->{msg};
}

sub hello_hello :Hook('bar') {
    my($self, $c, $args) = @_;
    'bog'
}
1;

helloメソッドではexample.plスクリプトの中で $obj->hello として呼び出される先となっていて、hello_helloメソッドはexample.plスクリプトの中で $obj->run_hook( 'bar' ) として呼び出されている先となっています。

それぞれ、Class::Componentにデフォルトで実装されているClass::Component::Attribute::MethodとClass::Component::Attribute::Hookを利用しています。

helloメソッドはexample.plで設定されたconfigを用いて戻り値を作成しています。

MyClass::Plugin::Baz

package MyClass::Plugin::Baz;
use strict;
use warnings;
use base 'Class::Component::Plugin';

sub baz :Method :News {
    my($self, $c, $args) = @_;
    'hello baz method'
}
1;

bazメソッドの戻り値はhello baz methodになるかと思いきや、MyClass::Attribute::Newsを利用しているため、どのような値を返したとしてもnewsになります。

Plugin1つにつき、Methodなどが1つしか書けないという訳ではなく、いくつでも書くことが出来ます。

名前空間の補間

load_componentsやload_pluginsに渡す引数は、自動的に名前空間を補間されて利用されます。

例えば今回のMyClassを例に取ると、HelloはMyClass::Plugin::Helloと補間してくれます。

もし、MyClass::Plugin::Helloが存在しなかった場合には@ISAの継承順に探索し、Class::Component::Plugin::Helloが存在すれば、それを利用します。

ComponentやAttributeに関しても同様にMyClass::(Component|Attribute)::*が無ければClass::Component::(Component|Attribute)::*を探します。

厳密に言うと@ISAの継承順ではなくClass::C3の探索ルールと殆ど同じアルゴリズムで探索を行ないます。

次回予告

今回は、Class::Componentのチュートリアルとして基本的な利用方法の紹介をしました。

次回以降から、Class::Componentを実装する上でのヒントとなった既存CPANモジュールの実装を交えながら、より実践的にClass::Componentを利用したプラガブルモジュール作成の方法を紹介していきます。

おすすめ記事

記事・ニュース一覧