Perl Hackers Hub

第60回動的なモジュールロードとの付き合い方(2)

前回の(1)こちらから。

動的なモジュールロードで実現できること

動的なモジュールロードをすると実現できることを紹介します。

コード量が少なくなる

ロードするモジュールの名前を動的に生成できるため、useでモジュールロードする場合と比べて書く必要のあるコード量が少なくなります。

たとえば次のように、繰り返し文で複数のモジュールをまとめてロードできます。

use Module::Load qw( load );
for my $module_name (qw/ Foo Bar /) {
    load('MyApp::' . $module_name);
}

動的なディスパッチ

コード量が少なくなることの中でも特筆すべき点として、利用するモジュールを値に応じて動的にディスパッチできることが挙げられます。

項目の多いマスタデータや、ユーザーからの入力など、プログラム外部から渡された値に応じて処理を分岐させたい場合に、コード量が大幅に少なくなるので便利です。

具体的な利用例として、ゲームでアイテムを使用する処理の実装を考えてみます。

データベースにある大量のアイテムデータと、アイテム使用時に実行される効果の処理の対応を愚直に条件分岐で実装すると大変です。しかし、次のようにアイテム名からアイテム効果モジュールのモジュール名を動的に生成し、生成したモジュール名のモジュールを動的にロードするようにすれば、少ないコード量で簡単にアイテムを使用する処理を実装できます。

use Class::Load qw( load_class );
use String::CamelCase qw( camelize );

sub use {
    my ($self, $user) = @_;
    my $module =
        'MyApp::ItemEffect::' . camelize($self->name);
    load_class($module);
    my $effect = $module->new(@_);
    $effect->execute($user);
}

起動/ロード時間の短縮

モジュールロードするタイミングを、モジュールを利用する直前にまで遅らせられるため、アプリケーションの起動やモジュールのロードにかかる時間を短くできます。MooType::Tiny、Class::Accessor::Liteなど、パフォーマンスを意識しているCPANモジュールや、巨大なWebアプリケーションでこの用途による利用をよく見かけます。

依存関係の動的な解決

依存関係を動的に解決できるので、条件に応じて依存するモジュールを変更したり、モジュールロードに失敗した場合の処理を書けます。また、モジュールが相互に依存していてもサブルーチン再定義による警告が発生しないように実装できます。

動的なモジュールロードの活用

これらの動的なモジュールロードで実現できることが、具体的にどう活用されているのかを見ていきましょう。

コード量を少なくする

コード量を少なくするために活用されている事例を見てみましょう。

Module::Findの事例─⁠─特定の名前空間に属するモジュールをすべて読み込む

Module::Findは、ある名前空間の下に属するモジュールをまとめてロードできるライブラリです。usesubで引数の名前空間直下に属するモジュールをロードし、useallで引数の名前空間に属するモジュールを再帰的にロードします。

use Module::Find qw( usesub useall );

# Foo::Bar, Foo::Baz, ...
my @sub_modules = usesub('Foo');

# Foo::Bar, Foo::Bar::Hoge, Foo::Baz, etc...
my @all_modules = useall('Foo');

Catalystのコンテナ─⁠─use忘れを防ぎ、モジュール名を短縮する

CatalystというPerl製のWAFWeb ApplicationFrameworkがあります。Catalystではアプリケーションの起動時にすべてのモデルモジュールをロードし、モデルクラスのインスタンスをキャッシュするコンテナのようなしくみがあり、各モデルの操作はキャッシュされたインスタンスを取得して行います。これにはuse忘れがなくなる、モジュール名が短縮された状態でメソッドを呼び出せる、一度生成したモデルクラスのインスタンスをキャッシュできるというメリットがあります。

実際にCatalystで作られたアプリケーションでどのようにモデルモジュールが使われるのかを見てみましょう。

package MyApp::Controller::Root;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' }
__PACKAGE__->config(namespace => '');

sub foo :Local :Args(1) {
    my ($self, $c, $name) = @_;
    my $country =
        $c->model('DB::Countries') ―(1)
            ->search({ name => $name }); ―(2)
    $c->stash(
        template => 'foo.tt',
        country => $country,
    );
}

このコードはCatalystのコントローラのコードになります。

前述したとおり、すべてのモデルモジュールは起動時にロードされるため、ここでモジュールロードをする必要はありません。

(1)では、起動時にロードしキャッシュしたモデルクラスMyApp::Model::DB::Countriesのインスタンスを取得しています。

(2)では(1)で取得したインスタンスからsearchメソッドを呼び出しています。

Catalystのコンテナ機能と似た事例にモデルモジュール専用のモジュールローダがあり、Catalystとは別のWAFのプラグインなどで見受けられます。これは起動時にすべてのモデルモジュールのロードをするのではなく、モデルモジュールのインスタンスを初めて取得しようとするタイミングでモデルモジュールのロードをします。

プラガブルなアーキテクチャの実装─⁠─Tengの事例

簡単にプラグインの実装と追加ができるようになるため、プラグインを追加できるアーキテクチャの実装にもよく動的なモジュールロードが活用されています。

例としてTengのプラグイン機構を簡略化したコードでプラガブルなアーキテクチャの実装方法を紹介します。

Tengは軽量なO/Rマッパで、メソッドを追加したりモンキーパッチ[1]を当てられるプラグイン機構が用意されています。たとえばTeng::Plugin::Countを使うと、COUNT関数でレコード数を取得するSQLを発行するメソッドが利用できます。

プラグイン機構の実装

Tengのようなプラグイン機構は、プラグイン名をモジュール名に変換してロードし、追加するメソッド(今回はプラグインモジュールの@EXPORTで指定されたメソッド)をモジュール側のシンボルテーブルに追加するしくみを用意することで実現できます。

lib/Module.pm
package Module;

use Module::Load qw( load );

sub add_plugin {
    my ($class, $plugin) = @_;
    my $package = "Module::Plugin::${plugin}";
    load($package);
    {
        no strict 'refs';
        for my $method ( @{"${package}::EXPORT"} ){
            *{$class . '::' . $method} =
                $package->can($method);
        }
    }
}

プラグインモジュールの実装

次に、プラグインモジュールを実装します。モジュールのパッケージ名はModule::Plugin::${プラグイン名}といった風に、Module::Plugin名前空間の下に属させます。このプラグインモジュールにモジュール本体に追加したいメソッドを実装し、メソッド名は@EXPORTに追加します。

lib/Module/Plugin/Hoge.pm
package Module::Plugin::Hoge;

our @EXPORT = qw( do_something );

sub do_something {
    my $class = shift;
    warn 'Do something';
}

プラグインモジュールの利用

プラグインモジュールを利用するには、⁠プラグイン機構の実装」で実装したadd_pluginメソッドにプラグイン名を渡します。

モジュール本体で直接プラグインを利用すると、モジュール本体を使っているすべてのコードがプラグインによる挙動変更の影響を受けるため、モジュール本体を継承したクラスを作りそこでプラグインを利用します。

lib/My/Module.pm
package My::Module;
use parent 'Module';
__PACKAGE__->add_plugin('Hoge');

プラグインを利用したならば、プラグインモジュールによって追加されたメソッドが呼び出せるようになっています。

main.pl
use My::Module;
My::Module->do_something();

フレームワークの実装─⁠─Mojoliciousのルーティングの事例

動的なモジュールロードを活用すると、簡単に機能の実装ができたり、複雑なルールによる動的なディスパッチを宣言的に記述できるため、フレームワークの実装にもよく活用されます。

例として、Mojoliciousのルーティングのしくみを紹介します。

MojoliciousはPerl製のWAFで、宣言的にルーティングを記述してリクエストに応じた柔軟な処理のディスパッチができます。次のコードのように、アプリケーション起動時に呼ばれるメソッド内でリクエストに応じて呼び出されるコントローラクラスとメソッド名を記述すると、リクエストが来たときにリクエストに応じてコントローラクラスをロードし、メソッドを呼び出します。

package SomeWebApp;

use Mojo::Base 'Mojolicious';

# アプリケーション起動時に呼ばれるメソッド
sub startup {
    my $self = shift;
    my $r = $self->routes;

    $r->get('/')->to('Root#root'); ―(1)

    my $user =
        $r->any('/user')->to(controller => 'user'); ―(2)

    $user->post('/add/:name')->to(action => 'add'); ―(3)
}

(1)では、ルートにgetメソッドでリクエストが来た際SomeWebApp::Controller::Rootクラスのrootメソッドを呼び出す指定をしています。

(2)では、/userにあらゆるメソッドでリクエストが来た際SomeWebApp::Controller::Userクラスのメソッドで処理することを指定しています。$user_routesにはその状態を保持したルーティングオブジェクトが格納されているので、この段落以降の処理ではSomeWebApp::Controller::Userクラスのどのメソッドで処理を行うかの指定をするだけで簡潔にリクエストに応じた呼び出す処理を割り当てることができます。

(3)では、/user/add/${name}[2]にpostメソッドでリクエストが来た際に呼ばれる、SomeWebApp::Controller::Userクラスのメソッドを指定しています。

このように動的なモジュールロードを活用すると、静的に記述すると冗長になるディスパッチの記述を宣言的にかつ簡潔に記述できます。

<続きの(3)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧