メソッドチェーンの話の続きの続き(エラー処理編)

メソッドチェーンを使うとデバッグしづらくなるんでないか、という疑問に対するMojo的な反論。

my $instances = Mojo::Loader->new->load('Book')->build;

という文があるとき、loadに失敗したらどうなるかを考えてみる。

よくあるメソッドのようにload('Book')がクラスないしオブジェクトを返す場合は、ロードにこけたら次のbuildを実行するオブジェクトがないといって怒られるのがふつうだ(or die $@などで判定しやすくなるよう、undef、あるいはその他の偽になる文字列などが返ってくることが多いため)。だから、evalでくるんだり、チェーンを切って逐一エラーチェックする必要がある。

Mojoのように基本的には自分を返し続けるチェーンなら、そうはならない。実際にMojo::Loaderのloadとbuildのソースを見てみよう。

sub load {
    my ($self, @modules) = @_;

    $self->modules(\@modules) if @modules;

    for my $module (@{$self->modules}) {

        # Shortcut
        next if $module->can('isa');

        # Load
        eval "require $module";
        croak qq/Couldn't load module "$module": $@/ if $@;
    }

    return $self;
}
sub build {
    my $self = shift;

    # Load and instantiate
    my @instances;
    foreach my $module (@{$self->modules}) {

        eval {
            if (my $base = $self->base)
            {
                die "SHORTCUT\n" unless $module->isa($base);
            }
            my $instance = $module->new(@_);
            push @instances, $instance;
        };
        croak qq/Couldn't instantiate module "$module": $@/
          if $@ && $@ ne "SHORTCUT\n";
    }

    return \@instances;
}

見ての通り、loadしたら$self->modulesの中にロードしたモジュールが入るわけだが、たとえこれが空だとしても、エラーにはならない。なにもロードしていないかのようにbuildを実行しておしまいである(何らかの事情でロードしたモジュールが壊れていた/newでインスタンス化できない場合はエラーで止まるが、これはそのまま実行すると先々意味不明のエラーのもとになるだけから仕方ないだろう)。

このくらい短い例だとありがたみが薄いから、もう少し長い例も見てみよう。今度はロードしたあと、ベースクラスがMyApp::Baseの場合だけインスタンス化する、という例だ。

my $instances = Mojo::Loader->new->load('Book')->base('MyApp::Base')->build;

この場合も、Bookのロードに失敗しようが、baseの条件にあわなかろうが、先の条件以外ではエラーで死ぬことはない。ただし、どこでこけたにせよ、どこかでこけていたなら、$instancesには空の配列が渡される。

ただ、ここで知りたいのは、この先の処理で使うインスタンスがとれるかどうかだけのはずだ。だから、必要ならこの次に

croak "load error: Book" unless @$instances;

とでも書いておけばアプリケーションレベルでのエラー処理としては十分ということになる。

#どうしてもどこでこけているか知りたければ、適宜チェーンを切って、$self->modulesの中身などをチェックすればよいだろう。

デバッグに不安を持たれた方々にも満足のいく解答になっていればよいのだが。