Perl Hackers Hub

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

(1)こちら⁠2)こちらから。

動的なモジュールロードで発生する問題とその対策

さて、⁠2)までは動的にモジュールロードをすることで実現できることを見てきましたが、動的なモジュールロードを行っていると良いことばかりあるわけではありません。注意して利用しなければ保守性が下がります。

(3)では動的にモジュールロードをすることで発生する問題と、その対策について解説していきます。

可読性が低くなる─⁠─動的にモジュール名を作らない

モジュールロードをする際に文字列連結や正規表現などで動的にロードするモジュール名を作っていると、ロードするモジュールの名前は処理を追わないとわからなくなるので、可読性が低くなります。

また、機械的にもコードを調べにくくなるため、コードが検索しにくくなったり、静的解析を利用したツールの恩恵にあずかれなくなる可能性があります。

可読性が低くならないようにするため、動的にモジュールロードする際はなるべくモジュール名を動的に作らないようにしましょう。動的に作る必要がある場合もなるべく単純なルールにします。複雑な正規表現やロジックでモジュール名を作るのは、特別な理由がない限り避けたほうがよいでしょう。

例外として、WAFのルーティングの記述など、宣言的にディスパッチルールを記述したい場合は、暗黙的な知識が増えることと引き換えに実装の簡単さと視認性が得られるので、複雑なロジックで動的なモジュール名の作る合理性があります。しかし、暗黙的な知識が増えるとその分開発者が覚えなければならないことが増えるため、文書化やチームへの知識の共有が必要ですし、それが本当に必要なのかもよく検討するべきでしょう。

循環する依存関係が作られる─⁠─依存関係をチェックする

循環するモジュールの依存関係があると、依存関係のあるモジュールのどこかが壊れたり変更があったりした場合に、依存関係のあるモジュールすべてに影響が及ぶ可能性があります。ですので、なるべくモジュールの依存関係は単方向にしたいです。

しかし、動的にモジュールロードを行っている箇所が多いと、モジュールの依存関係が混沌としていつの間にか循環する依存関係を作ってしまいがちです。

そうならないように、できるだけモジュールの依存関係が単方向になるようチェックするしくみを導入するべきでしょう。

静的にモジュールロードしている場合

静的にモジュールロードしている場合は、次の理由により依存関係のチェックがしやすいため、循環する依存関係は作られにくいです。

  • 循環している依存関係があればサブルーチン再定義の警告が出る
  • モジュールロードするコードはファイル上部にまとめて書かれていることが多いので、どのモジュールに依存しているかが視認しやすい
  • Perl::PrereqScannerなど静的解析を利用したツールによって依存しているモジュールの検出がしやすいため、依存関係のチェックがしやすい

動的にモジュールロードしている場合

しかし、動的にモジュールロードしている場合は、次の理由により循環する依存関係が作られがちです。

  • 動的に依存関係を解決するため、循環する依存関係があってもサブルーチン再定義の警告が出ない
  • あらゆる箇所から自由に別モジュールの処理を呼べるため、どのモジュールがどのモジュールに依存しているかが視認しにくくなり、混沌とした依存関係になる
  • 静的解析を利用したツールによって依存しているモジュールの検出ができないか、できたとしても独自に静的解析する処理を書かないと依存しているモジュールの検出ができないため、精度の高い依存関係のチェックがしにくい

循環する依存関係を作らないようにするには

循環する依存関係を作らないようにするには、どうにかして依存関係が単方向になっているかをチェックする必要があります。どのモジュールがロードされるか静的解析でわかる場合は、自分で静的解析する処理を書いて依存関係をチェックできます。静的解析でわからない場合は、完璧な依存関係のチェックは諦め、独自のモジュールローダを作りcallerで呼び出しもとを調べて記録、循環する依存があれば警告を出すしくみなどを用意するとよいでしょう。

しかし、上記のような依存関係をチェックするしくみを用意し運用するのはかなり大変です。依存関係をチェックするしくみが用意できないようなら、できるだけ静的にモジュールロードしたほうがよいでしょう。

実行までモジュールロードできるかわからない─⁠─すべてのモジュールがロード可能かテストする

依存しているモジュールが文法エラーなどでモジュールロードに失敗する状態になっていても、実際にコードが実行されるまでモジュールロード可能かわからなくなります。ですので、テストが書かれていない箇所が本番環境で急に使えなくなる、などといった現象が起きる可能性があります。

最近ではIDEIntegrated Development Environment統合開発環境)やエディタが高機能になっており、開発中に文法エラーをチェックして気付けるのであまり大きな問題にはなりませんが、それでもすべてのモジュールがロード可能なのかの保証がなく不安です。

すべてのモジュールがロード可能であることを保証するには、すべてのモジュールがロード可能かをチェックするモジュールを使ってテストをするとよいでしょう。Test::UseAllModulesはそういった機能を提供するモジュールの1つで、デフォルトではall_use_ok関数でlib以下のすべてのモジュールがロード可能かをテストします。

保守性が下がる使い方ができる─⁠─静的なモジュールロードをする方法で代替できないか検討する

ここまでで、動的なモジュールロードはその柔軟性により注意して利用しなければ保守性が下がるということを見てきました。

静的にモジュールロードする場合は逆に柔軟性がない分、保守性が下がる書き方はしにくいので、普段は静的にモジュールロードをする方法で実装し、必要なところだけ動的にモジュールロードをしたいところです。

実は動的なモジュールロードでしか実現できないと思われることでも、記述量が少し多くなるものの静的なモジュールロードをする方法で代替できる場合があるので、その方法を紹介します。

モジュール名を短く記述したい場合─⁠─定数かaliasedでエイリアスを作る

モジュール名を短く記述してクラスのメソッドを呼び出したい場合、Catalyst風のコンテナやモジュールローダを使うのではなく、定数でモジュール名のエイリアスを作るか、aliasedを利用してモジュールロードとモジュール名のエイリアスを作ることでメソッドを呼び出します。

コンテナやモジュールローダを利用する場合と違ってuse忘れは防ぐことができませんが、定数かaliasedでモジュール名のエイリアスを作る場合はエイリアスの中身のモジュール名文字列がコンパイルフェーズにインライン展開されるため、実行時のオーバーヘッドはありません。

use constant Hoge => 'MyApp::Foo::Hoge';
# 'MyApp::Foo::Hoge'->do_something() と同じ
Hoge->do_something();

use aliased 'MyApp::Bar::Fuga';
# 'MyApp::Bar::Fuga'->do_something() と同じ
Fuga->do_something();

拡張性のあるアーキテクチャを作りたい場合─⁠─プラグインを外部で静的にロードし渡す

プラガブルなモジュールなど拡張性のあるアーキテクチャを作る場合、プラグインの追加を簡単にすることが重要でなければ、プラグインを外部で静的にロードして渡すようにして実装できます。

この実装方法でプラガブルなアーキテクチャの実装─⁠─Tengの事例で紹介したプラグイン機構のコードを書き換えると、次のコードになります。変わったのは2ヵ所で、プラグイン名からモジュール名を作る処理と、プラグインモジュールをロードする処理がなくなっています。

lib/Module.pm
package Module;

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

このプラグイン機構でプラグインを使うには、プラグインモジュールを外部でuseし、add_pluginメソッドにプラグインモジュールのモジュール名をそのまま渡します。

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

動的にディスパッチさせたい場合─⁠─利用するモジュールとの対応を自動生成する

動的にディスパッチしている箇所は、利用するモジュールをすべて事前にuseしておき、外部からの値に応じて利用するモジュールの対応を書くことで代替できます。数があまりにも多い場合、対応の部分のコードだけ自動生成するしくみを用意する手もあります。工数、拡張性と保守性のトレードオフになるので、状況に応じてどちらで実装するか判断するとよいでしょう。

たとえば、動的なディスパッチの項で示した、動的にディスパッチさせているコードを対応表を書く方法で書き換えると、次のコードになります。

use MyApp::ItemEffect::Potion;
use MyApp::ItemEffect::Ether;
use MyApp::ItemEffect::Elixir;

sub use {
    my ($self, $user) = @_;
    state %effect_module_of = (
        potion => 'MyApp::ItemEffect::Potion',
        ether => 'MyApp::ItemEffect::Ether',
        elixir => 'MyApp::ItemEffect::Elixir',
    );
    my $module = $effect_module_of{ $self->name };
    my $effect = $module->new(@_);
        $effect->execute($user);
}

アイテム名とアイテム効果モジュールの対応を%effect_module_ofで定義し、渡されたアイテムオブジェクトの名前をキーとして%effect_module_ofからアイテム効果モジュールのモジュール名を取得しています。

依存関係を動的に解決したい場合

依存関係を動的に解決したい場合も、静的にモジュールロードする方法で書き換えられる場合があります。

特定の条件でのみモジュールロードをしたい場合はifprovideプラグマなどが利用できます。たとえば、Perlのバージョンが5.30以下のときだけSome::Moduleをロードする処理は次のように書けます。

use if $] < 5.030000, 'Some::Module';

また、BEGINコードブロックや、useされたときによばれるimportメソッドに処理を書くことで、コンパイルフェーズで処理を実行させられるので、それらの活用を検討するのもよいでしょう。たとえばBEGINコードブロック内でModule::Findを利用すると、コンパイルフェーズ時に特定の名前空間に属するモジュールをまとめてロードできます。

BEGIN {
    use Module::Find qw( usesub );
    usesub('MyApp');
}

しかし、BEGINコードブロック内やimportメソッド内で複雑な処理が実行されたり、副作用のある処理が実行されると、保守性を高めるために静的にモジュールロードしているはずなのにかえって保守性が下がるので、ほどほどに利用しましょう。

まとめ

動的なモジュールロードは実行時にモジュールロードをするため柔軟性があります。そのためフレームワークやプラガブルなアーキテクチャの実装に向いており、少ないコード量で簡単に機能が実装ができる体験を開発者に提供してくれます。また、ロード時間の短縮や動的な依存関係の解決など、動的なモジュールロードでないと実現できないこともあります。

しかし注意して利用しなければ保守性が下がります。動的なモジュールロードをしたくなった場合は、静的なモジュールロードをする方法で代替できないかを検討してみましょう。そのうえで、フレームワークやプラガブルなアーキテクチャを実装したい場合、動的なモジュールロードでしか実現できないことがある場合は、なるべく保守性が下がらない工夫をしましょう。

さて、次回の執筆者は野口啓介さんで、テーマは「Amazon ECSとGithub Actionsを使ったDockerアプリケーションの自動デプロイ」です。

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

おすすめ記事

記事・ニュース一覧