DIとは何か 番外編 - DIコンテナを使わずにModelのTestabilityを高める方法 -

DIコンテナは使わないけれども、「DIパターン + コラボレータ(依存性)のデフォルト設定 + Catalyst::Model::Loader」の構成にすることで、各コンポーネント単体テストが可能になり、Testabilityを高める事ができます。

「DI + コラボレータのデフォルト設定」はMooseを使うととても簡単にできます。以下にコードで例を示しましょう。

package MyApp::Service::Sample;
use Moose;
use MyApp::Service::Dependency;
use Perl6::Say;

has 'collaborator' => (
    is      => 'ro',
    default => sub {
        MyApp::Service::Dependency->new;
    }
);

no Moose;

sub helloworld {
    my $self = shift;
    $self->hello;
    $self->collaborator->world;
}

sub hello {
    say 'hello';
}

__PACKAGE__->meta->make_immutable;

1;

ここは依存関係のあるオブジェクトcollaboratorをMooseのattributeにして、そのattributeのdefault値として、依存性のあるオブジェクトを生成しています。ここが肝です。

Mooseのdefault機能は、ここで活きてきます。DIでいう依存性のオブジェクトをdefaultで設定します。テスト時にはこれを上書きしてテスト用のものをConstructorに渡して差し込めばそのクラスは単体でテストができるようになります。Sub区ラッシングしてdefaultを上書きしたものに対してテストをするのでもよいです。

この方法は、defaultでnewするためにuseしてしまうわけで、完全なDIパターンとはいえませんが、DIのメリットを享受できるので妥協できる範囲だと思います。

この手法の良い点

これにより、各コンポーネントはDI可能な設計になるので、コンポーネント単体テストができるようになり、Testabilityが高まります。要するに、コラボレータがattributeになって、Constructor Injectionできるようになっているため、テスト時にはコラボレータを差し替えてテストをすることができるという意味です。Constructor Injectionで置き換える他にもサブクラス化して、defaultを上書きするだけでも、テストができるようになります。

これだけでもメリットは大きいですね。外部環境に依存する物はMooseのattributeにしてしまえばいいわけです。それだけで、自然とDI可能な設計になっていきます。

この手法でできないこと

環境のInjectionによる結合テストは難しくなります。

幾つかのモジュールを複数人で開発している場合、コンポーネント間の結合テストが必要になります。コンポーネント(全体または一部)の結合テストをするときに、モジュールを繋ぎ合わせた単位で結合した単位での動作に問題がないかをテストするときに使います。

複数のコンポーネントにまたがる結合テストを外部依存モジュールなしでテストしたい場合もあるでしょう。

例えば、

として、結合したコンポーネントのテストをするなどの場合です。

外部環境のInjectionというのは、コードに手をいれずに、コード全体を、Memcachedではなくメモリ上に、MySQLではなくSQLiteなどに一括で切り替えたりということです。各コンポーネントに依存性のデフォルト値が設定されている場合は、一括でそれらの設定を置き換えるのが難しいため、このようなテストは難しくなってきます。

外部環境だけでなく、DI Containerで一括でwiringすることができなくなるものを「DI+デフォルト値」の方式では置き換えにくいという話です。

POPOモデルのWebアプリケーションへの繋ぎ込み方

上記のPOPOのモデルをWebアプリケーションに繋ぎ込むには以下のようにします。

  • Catalyst::Model::Loaderを使って、MyApp::Service以下のパッケージをCatalystのモデルにする。
  • CatalystのControllerでは$c->model('Service::Sample')->hello(); といったようににすることでPOPOのオブジェクトを実行
  • Mooseのdefaultsで依存するコラボレータが自動で設定されるため、Service::Sampleのクラスは、依存関係の解決された状態で取得できるため、そのクラスを実行する

余談になりますが、CatalystCon#1で話をしたように、Catalyst::Model::Adaptorのようにラッパとモデルが1:1になってしまうといったものを使うと、ラッパが爆発してしまいよくないですよね。この一つの解が、Catalyst::Model::Loaderのようなものであり、またDIコンテナのAutowiring機能であったりします。

CatalystのModelにしなくても、ServiceLocatorをつくる形にしてもいいとは思います。CatalystのModelの形にしておけば、自動的にServiceクラスはSingletonになるのと、Service Locatorを作る手間がなくなるというメリットがあるからしているということになります。

まとめ

今回はDIコンテナを使わずによりTestabilityの高いPOPOモデルを構築する方法として、

  • DIのパターンとMooseのコラボレータによるdefault設定を利用してPOPOモデルを構築する方法
  • 作成したPOPOモデルをCatalyst::Model::LoaderでWebアプリケーションのModelとして繋ぎ込む方法

を紹介しました。DIコンテナの複雑さを持ち込む事無く、DIのメリットを享受できるという点でよい手法ではないかと思います。これは、DIコンテナを使わずにTestabilityの高いモデル層を構築する一つの方法だと考えています。

DIコンテナを使うとこの手法でできないことができるようになります。ですが、動的言語でDIコンテナを使う事によるデメリットもでてきます。本編では、LLにおけるDIコンテナ使用のメリット、デメリットについても紹介していきたいと思います。