MPがありません。

$liiu->mp == 0

Perlにおける動的なモジュールロードのメリットとデメリットについて

概要

  • Perlにおける動的なモジュールロードのメリットとデメリットについて、そしてどういう場合に動的なモジュールロードを行うといいか、僕が考えていることを記します
  • 例のコードや具体例の説明などはPerlの文脈で行っていますが、動的なモジュールロードが可能な言語ならどの言語でも似たような利点と欠点が発生することになると思います

ご指摘などあればコメント欄で教えていただけると幸いです。

動的なモジュールロードを行うことによるメリット

アプリケーションの起動にかかる時間を短縮できる

モジュールをロードするタイミングをコードを実行する直前にまで遅らせることができるので、アプリケーションの起動にかかる時間を短くすることができます。
高速化を意識しているCPANモジュールなどで、このようなコードを見かけることが多い気がしています。

例えば Class::Accessor::Lite ではこのようなことをしています。
https://metacpan.org/release/Class-Accessor-Lite/source/lib/Class/Accessor/Lite.pm#L7

コード量を少なくできる

例えば、下記のコードのようにたくさんのモジュールを1個1個useするのは結構面倒だったりしますが、動的にモジュールをロードする場合はfor文で一括でロードできちゃったりします。

use MyApp::Foo;
use MyApp::Bar;
use MyApp::Baz;
use Module::Load qw( load ); # コアモジュールのモジュールローダーです
load "MyApp::$_" for qw( Foo Bar Baz );

また、Catalystのモジュールローダーのように*1、useなどモジュールをロードする関数を呼び出さずに、モジュールローダーを介して直接他のモジュールのメソッドを呼び出させることもできます。
この場合、長いモジュール名のモジュールをある程度短い名前で呼び出せるというメリットもあるかなと思います。

use MyApp::Model::Foo;
sub hoge {
    my $self = shift;
    MyApp::Model::Foo->do_something();
}
use Module::Load qw( load );

sub hoge {
    my $self = shift;
    # モジュールローダー(modelメソッド)は MyApp::Model::Foo をまだロードしていない場合ロードして、
    # 'MyApp::Model::Foo' という文字列を返す。 
    # -> 演算子は左側の文字列をパッケージ名としてメソッドを呼びだそうとするので、 
    # MyApp::Model::Foo->do_something() と同じ処理を行う
    $self->model('Foo')->do_something();
}

sub model {
    my ($self, $module_name) = @_;
    my $package_name = 'MyApp::Model::' . $module_name;
    load $package_name;
    die $@ if $@;
    return $package_name;
}

モジュールをロードする順番を動的に決めることができる

モジュールロードを実行時に行うと、モジュールをロードする順番を動的に決めることができたり、状況に応じて依存するモジュールを変えれたりします。

例えば(そもそもこのような設計はよくないのですが)、以下のコードのように相互に依存するモジュールがある場合、静的に互いのモジュールをロードしていると、コンパイル時に互いを再帰的にロードしようとしてしまうためか(?)、それぞれのファイルのコンパイル時に Subroutine redefined の警告が出ます。

Foo.pm
package Foo;

use strict;
use warnings;
use feature 'say';
use Bar;    

sub hoge {
    say 'hoge';
}
    
sub fuga {
    Bar::hogera();
    say 'fuga';
}

1;
Bar.pm
package Bar;

use strict;
use warnings;
use feature 'say';
use Foo;

sub piyo {
    Foo::hoge();
    say 'piyo';
}

sub hogera {
    say 'hogera';
}

1;

しかし、以下のように依存しているモジュールが必要になる直前に動的にモジュールロードを行った場合、そのような警告は出ません。

Foo.pm
package Foo;

use strict;
use warnings;
use feature 'say';
    
sub hoge {
    say 'hoge';
}
    
sub fuga {
    require Bar;
    Bar::hogera();
    say 'fuga';
}

1;
Bar.pm
package Bar;

use strict;
use warnings;
use feature 'say';

sub piyo {
    require Foo;
    Foo::hoge();
    say 'piyo';
}

sub hogera {
    say 'hogera';
}

1;

動的なモジュールロードを行うことによるデメリット

依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからない

依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからないため、アプリケーションの起動後にSyntax errorなどコンパイル時にわかるようなエラーで一部の機能が使えない・・・といった事態が発生してしまう恐れがあります。
全てのモジュールのテストが書かれていればこのようなデメリットを無視できますが、通常必ず全てのモジュールにテストが用意されていることに期待をすべきではないでしょう。

静的解析を利用したツールの恩恵にあずかれなくなる。また、読みにくいコードになる恐れがある

これはモジュールロード時にモジュール名を動的に組み立てている場合に発生する問題で、動的なモジュールロードによる問題というよりは動的にトークンを組み立てる問題と言えそうですが、動的にモジュールロードを行うときに一緒によく起きがちな問題なので取り上げます。

例えば先ほど上げたCatalyst風のモジュールローダーでモジュールをロードしている場合、静的解析ではどのようなモジュールをロードしているかはわからないため、コードジャンプやメソッド名を補完するようなツールの中には正常に動作しないものがけっこうでてきます。
そのようなツールに特別な対応させることで動作させることは可能かもしれませんが、その分の対応コストがかなりかかるでしょう。

sub hoge {
    my $self = shift;
    # modelメソッドがどういうモジュール名を返すのかは,
    # 実際にコードを実行してみないとわからない!
    $self->model('Foo')->do_something();
}

また、動的にモジュールロードされていることを知らない人がコードを読んだ場合、依存モジュールがどこで使われているのかがわかりにくくなってしまう恐れがあります。

モジュール間の依存関係がわかりにくくなってしまって、変な設計をしてしまう原因になる

  • モジュール間で相互依存が発生している場合も警告がでないので気づけなくなってしまう
  • モジュール間の依存関係を機械的に解析することが難しくなる
  • 個人的な感覚ですが、コードの途中でモジュールがロードされているより、モジュールのある一箇所で依存しているモジュールがまとめてuseされている方が依存関係がわかりやすいと思います

他にも、CHECK, INITコードブロックに書かれたコードが実行されないなどといった細かい問題などもあります。

所感

僕としては、特に長期的にメンテナンスしていく必要のあるコードや他の人が読むようなコードおいてはデメリットの方が目立ってしまうため、プロダクトではなるべく動的なモジュールロードは行わない方がよい開発体験が得られるのではないかと思っています。
動的なモジュールロードを行ってもよい場面は、個人的に使う本当に使い捨てのスクリプトを書くような場面や、CPANモジュールを作るときにアプリケーションの起動が遅くならないようにしたい場合や、Plackのようなエンジンを作るなどといった場合に限られるのではないでしょうか。