Orochi紹介B!

Orochiってなんじゃらほい

Orochi (http://search.cpan.org/dist/Orochi) は一種のDependenc Injection(DI) フレームワークです。それぞれなんらかの依存関係があるオブジェクト群を初期化するコードを書くのに飽き飽きしていたので、書きました。

なお元々Bread::Board (http://search.cpan.org/dist/Bread-Board) にこの機能を追加したかったので、将来的に同等の機能がBread::Boardに実装された場合はそちらと統合する可能性が高いです。

Orochi使用例

例えば以下のよう構成のシステムがあったとします。

依存関係ダイアグラム
====================

                requires   --------------  requires
                ---------> | MyApp::Log |-----------> $file
   ---------    |          --------------
   | MyApp |----|
   --------     |          -----------------
                ---------> | MyApp::Schema | -------> @connect_info
                requires   -----------------

MyAppはLogオブジェクトとDBIx::Classスキーマに依存し、それらも$fileと@connect_infoにそれぞれ依存すると仮定します。これを実装するには以下のようなコードを書くことになります:

package MyApp;
use Moose;
use namespace::autoclean;

has log => (
    is => 'ro',
    isa => 'MyApp::Log',
    required => 1,
);

has schema => (
    is => 'ro',
    isa => 'MyApp::Schema',
    required => 1,
);

sub run {
    ....
}

__PACKAGE__->meta->make_immutable();

1;
package MyApp::Log;
use Moose;
use namespace::autoclean;

has file => (
    is => 'ro',
    isa => 'Str',
    required => 1,
);

__PACKAGE__->meta->make_immutable();

1;
package MyApp::Schema;
use Moose;
use namespace::autoclean;

extends 'DBIx::Class::Schema'

1;

このような構造を用意すると、依存関係を満たして初期化するために以下のようなコードを書く必要があります。

use MyApp;
use MyApp::Log;
use MyApp::Schema;

my $log = MyApp::Log->new(file => "/path/to/log.txt" );
my $schema = MyApp::Schema->connection('dbi:mysql:dbname=foo', 'user', 'password');
my $app = MyApp->new(
    log => $log,
    schema => $schema
);
$app->run();

この程度であれば一個一個手で書いていけば済む話ですが、依存関係が増えていくにしたがい管理するオブジェクト数とともに書かなければいけない事項があ指数関数的に増えていきます。

そうなってしまっては後から依存関係を追加・変更したりする場合に困ってしまいます。また、例えばWAF内で一旦その初期化を記したとしてもコマンドラインツールから同じオブジェクト群を作成するコードを用意しなければなりません。もちろんやってできないことはありませんが、かなり面倒くさくなってきます。

Orochiはこの作業をなるたけ1回だけ記してあとは自動的にオブジェクト群を生成できるようにするためのツールです。

Orochi の基本

Orochiは前項で紹介した仕組みの下位レイヤーを構成します。この部分は詳細を説明してもあまりおもしろくありませんのでざっくり割愛します。基本的な動作としては以下のようになります:

my $orochi = Orochi->new();

# 遅延評価でMyClass->new(\%args)を実行してオブジェクトに転換して
# 返すよう指定
$injection = $orochi->inject_constructor( $key => (
    class => 'MyClass',
    args  => \%args
) );
        
$object    = $injection->expand(); # この時点で展開
$object    = $orochi->get($key);   # 同等。$keyから$injection
                                   # オブジェクトを検索してexpand()する

# 後でそのままの値を返すよう指定
$injection = $orochi->inject_literal( $key => $value );
$value     = $injection->expand(); # $valueを返すだけ
$value     = $orochi->get($key);   # 同等

# bind_value は inject_* で指定した値をexpand()したものを遅延評価する。
# この場合、 $class->new({ arg1 => $arg1, arg2 => $arg2 }) が
# get()/expand() 時に呼ばれる
$orochi->inject_constructor(
    $key => (
        class => $class,
        args  => {
            arg1 => $orochi->bind_value( $key_for_arg1 ),
            arg2 => $orochi->bind_value( $key_for_arg2 ),
        }
    )
);

キモはinject_constructorでコンストラクタとその引数を指定して遅延評価できることと、引数そのものも同じ仕組みで遅延評価し依存関係の関係性だけを表記できることです。

MooseX::Orochi

Orochiの最終的な目的はMooseオブジェクト群を自動的にフレームワークに登録して、初期化が終わった状態でインスタンスを自動的に作成してもらう事です。Orochiに同梱されているMooseX::Orochiを使用するとMooseクラスにOrochi要のインジェクションルールを書き込んで置くことができるようになります:

package MyApp;
use Moose;
use MooseX::Orochi;
use namespace::autoclean;

# 以下の宣言は、後に
#   MyApp->new( {
#       log =>  .... # $orochi->get('myapp/log') を評価した結果
#       scuema =>  .... # $orochi->get('myapp/schema') を評価した結果
#   } );
# と評価されるよう指定しています
bind_constructor myapp => (
    args => {
        log => bind_value 'myapp/log',
        schema => bind_value 'myapp/schema',
    }
);

sub run { ... }

__PACKAGE__->meta->make_immutable();

1;

このように "use Moosex::Orochi" とすることにより、現在のスコープで "bind_constructor" と "bind_value" というDSL/関数が使用可能になります。

bind_constructorは前項のinject_constructorと同じように特定のキーと現在定義中のクラスのインスタンスを結びつけます。

package MyClass;
bind_constructor $key => ( %params );

この定義をすることによって、後に以下のようにインスタンスを取得したい旨を宣言しているわけです:

my $obj = $orochi->get($key)

なお、%paramsにクラスを指定する必要はありませんので、arg1とarg2が必要なMooseオブジェクトを定義するには

package MyClass;
use Moose;
sue MooseX::Orochi;
use namespace::autoclean;

bind_constructor 'myclass' => (
    args => {
        arg1 => bind_value 'myclass/arg1',
        arg2 => bind_value 'myclass/arg2'
    }
);

というようにすれば、OrochiはMyClassの引数の設定をしてくれます。もちろんmyclass/arg1とmyclass/arg2は別途注入する必要があります。

他のクラスも同じ仕組みを使って設定を記入していきます:

package MyApp::Log;
use Moose;
use MooseX::Orochi;
use namespace::autoclean;

bind_constructor 'myapp/log' => (
    args => {
        file => bind_value 'myapp/log/file'
    }
);

__PACKAGE__->meta->make_immutable();

1;
package MyApp::Schema;
use Moose;
use MooseX::Orochi;
use namespace::autoclean;

extends 'DBIx::Class::Schema';

bind_constructor 'myapp/schema' => (
    args => bind_value 'myapp/schema/connect_info',
    deref_args => 1,
);

__PACKAGE__->meta->make_immutable();

1;

これができたところで、inject_class() メソッドを使って、クラスの中に保存された設定をOrochiインスタンスに注入すれば、あとはget() で作成されたインスタンスを取得するだけです:

use Orochi;

my $o = Orochi->new();

# クラス定義から依存関係等を注入
$o->inject_class('MyApp');
$o->inject_class('MyApp::Log');
$o->inject_class('MyApp::Schema');

# このあたりは設定ファイルから読み込むのが吉
$o->inject_literal(
    'myapp/schema/connect_info' => 
        [ 'dbi:mysql:dbname=foo', 'username', 'password' ],
);
$o->inject_literal(
    'myapp/log/file' => '/path/to/log.txt',
);

my $app = $o->get('myapp');
$app->run();

なお、ここまでの説明はMooseX::OrochiのPODにも書いてありますのでそちらもあわせて参照してください。

MooseX::Orochiの継承

一度特定のクラスにMooseX::Orochiの設定を施せばその子クラスも同じ設定を引き継ぐ事ができます。

package Parent;
use Moose;
use MooseX::Orochi;
use namespace::autoclean;

bind_constructor 'myapp' => (
    args => {
        arg1 => bind_value 'myapp/arg1',
        arg2 => bind_value 'myapp/arg2',
    }
);
package Child;
use Moose;
use namespace::autoclean;

extends 'Parent';
use Orochi;

my $orochi = Orochi->new();
$orochi->inject_literal('myapp/arg1' => 'value1');
$orochi->inject_literal('myapp/arg2' => 'value2');
$orochi->inject_class('Child');

my $child = $orochi->get('myapp');

このようにすることでMooseX::Orochiの設定を引き継ぐ事ができるのでオブジェクトの使い回しも簡単にできます。子クラスでbind_constructorを使用すれば子クラス特有の設定でオーバーライドすることも可能です。

CatalystでOrochiを使う

Catalystは元からある程度の自動初期化機能を搭載していますが、それはあくまでCatalystというコンテキスト内であって、それ以外のコンテキストでは同様の仕組みは使えません。なので私はいわゆるアプリケーションロジック部分をCatalystから切り離し、Orochiを通して オブジェクト群を作成しています。

この仕組みをCatalyst内で簡単に扱うために Catalyst::Model::Orochi(http://search.cpan.org/dist/Catalyst-Model-Orochi) もCPANに登録されており、Orochiを設定ファイルからコントロールすることができます。

package MyApp::Model::Orochi;
use Moose;
use namespace::autoclean;

extends 'Catalyst::Model::Orochi';

__PACKAGE__->meta->make_immutable();

1;

使用する際には model() 関数からOrochiインスタンスにアクセスすることができます:

package MyApp::Controller::Root;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' }

sub hoge :Local {
    my ($self, $c) = @_;

    my $thing = $c->model('Orochi')->get('whatever');
}

__PACKAGE__->meta->make_immutable();

1;

このようなモデルを設定しておき、必要なオブジェクト群にMooseX::Orochiを適用するだけであとは基本的にmyapp.yaml内に記述を追加するだけで様々な値を注入することができます:

Model::Orochi:
  injections:
    key1: value1  # リテラル値を注入
    key2: value2
    key3: value3
  classes:
    - Class1      # 明示的に指定されたクラスを注入
    - Class2
    - Class3
  namespaces:
    - MyApp::API  # MyApp::API 内の全てのMooseX::Orochiクラスの
                  # 定義を注入

この仕組みを使うと、1行も初期化コードを書かずにオブジェクト群を生成することができます

まとめ

本稿の最初に書いた通り、もうオブジェクト群を初期化するのにとんと嫌気がさしていたのでこの仕組みは実際にプロダクション環境で使い始めています。以下の注意点等が問題にならないようであれば、是非一度さわってみてください!

  • 使いどころ
    • お互い依存関係のあるオブジェクト群が比較的多くある
    • Catalyst(もしくはその他のWAF)の中だけでなく、違うコンテキスト(例:コマンドラインツール)などでも同じオブジェクト群の初期化が発生する
    • 正直初期化コードをもう書きたくない人向け
  • 注意点
    • DIは全てのアプリケーションで必要なわけではないので、(人的、リソース的)コストとのバランスを考えて導入したほうがいい
    • Mooseを使わないなら、Orochiも使わない
    • Orochiは再帰的な依存関係の自動解決しません。その場合はOrochi::Injection::Setterを使います
    • MooseX::Orochiを使うと基本的に1クラス=1インスタンスになります。複数必要な場合は違う方法で指定する必要があります

明日は・・・うほ!dankogaiさんです!