PerlのDIコンテナーであるOrochiは、作者である牧さん(lestrratさん)ご本人がFukuoka.pmのFukuoka Perl Workshop #14のスライドのp.46で「循環依存の解決は実装できてない」と書いていらっしゃいます(参考:平田さんの「Fukuoka Perl Workshop #14に行ってきた」の記事)。
ただし、これは私にとっては特段の制約事項とは感じません。循環依存するようなクラスは、場合にもよりますが、私の場合には大抵クラス設計を誤って陥る泥沼状態であるからです。
とはいえ、クラスが複雑に絡み合う事態は起こり得るので、依存関係には気を付ける必要があります。ただ単に依存がぐるぐる回ってバターにならないようにすることは勿論ですが、依存関係を実際に注入する処理でも気を付ける必要があります。この処理は、dannさんの記事の通り、Java界隈ではwiring(ワイヤリング)という用語が充てられます。
で、そのwiringで気を付けるというのは、具体的には、inject_classの実行順が依存の逆順であってはならない、という点です。当たり前といえば当たり前なのですが、うっかりしていました。
以下はその備忘録と解説です。
Windowsで通ったテストがUbuntuで通らない
インスタンスが入って来ない
事の発端は、拙作DBICx::Modeler::Generatorのテストです。なお、このモジュールについては、CPANに上げる価値があるのか、CPANモジュール足り得る品質を満たしているのかをもう少し見極める必要があると考えています。
さて、自宅での専らの開発環境がUnix系(2割)ではなくWindows(8割)であるという、geekのgの字も見えない一般人な私は、Windows環境でテストを終えたつもりになりました。いざUnix系環境でのテストと、石垣さん(charsbarさん)が紹介している、chmodがちゃんと動くmake distを行おうと勇躍してUbuntuを立ち上げた私は、以下のようなエラーでテストに躓いてしまいました。
Attribute (tree) does not pass the type constraint because:
Validation failed for 'DBICx::Modeler::Generator::TreeLike' failed
with value Orochi::Injection::BindValue=HASH(0x5921b8)
要はtreeアトリビュートではTreeLikeインターフェース(Moose::Role)を満たす値を想定していたのに、蓋を開けてみたらそんなインスタンスじゃなくてOrochi::Injection::BindValue=HASH(0x5921b8)という値が入っていたよ、ということです。
依存が未解決であるという印
実はこの問題には以前にもはまったことがあって、依存が未解決の値を使おうとするとこうなります。インスタンスだけでなくて、例えばStrなどの型を想定していても同様です。
実際のDIの定義(MooseX::Orochiでエクスポートして使うbind_constructor)と注入処理(ワイヤリング)部では、そのtreeアトリビュートを規定する/DBICx/Modeler/Generator/Treeレジストリーだけを着目すると確かに問題なさそうに見えます。
例えばこの場合はv0.00タグで、
- 依存関係をクラスの
metaアトリビュートへ設定する箇所 DBICx::Modeler::Generator::Driver::SQLiteの54行目- 依存を注入する(ワイヤリングする)箇所
DBICx::Modeler::Generator::CLIの252行目や、実際のクラス名群をこしらえている182行目付近
がこれにあたります。
しかし、注入時の順番にも注意が必要なのです。v0.0001タグの同310行目に書きました通り、このアプリケーションは7段の依存関係があります。
DBICx::Modeler::GeneratorDBICx::Modeler::Generator::SchemaDBICx::Modeler::Generator::DriverDBICx::Modeler::Generator::ModelDBICx::Modeler::Generator::PathDBICx::Modeler::Generator::TreeDBICx::Modeler::Generator::Class
無駄にクラスを量産しすぎたきらいがありますが、ともあれ、この順番を覆して(以下のような順番で注入して)しまうと、場合によっては値が未解決のままインスタンスを生成しようとしてしまいます。
return [
qw(
DBICx::Modeler::Generator
DBICx::Modeler::Generator::Class
DBICx::Modeler::Generator::Model
DBICx::Modeler::Generator::Path
DBICx::Modeler::Generator::Schema
DBICx::Modeler::Generator::Tree
),
'DBICx::Modeler::Generator::Driver::' . $self->driver,
];
ちゃんと末端から根本の順に注入する
ということで、葉から枝へ、幹へ、値へというように末端から順にインスタンスを生成していかなければなりません。
v0.0001での同182行目付近では、以下のように真っ当な順番にしました。
return [
qw(
DBICx::Modeler::Generator::Class
DBICx::Modeler::Generator::Tree
DBICx::Modeler::Generator::Path
DBICx::Modeler::Generator::Model
),
'DBICx::Modeler::Generator::Driver::' . $self->driver,
qw(
DBICx::Modeler::Generator::Schema
DBICx::Modeler::Generator
),
];
これで、クラス間での依存関係は循環していないのに、依存の逆順でインスタンスを生成しようとしてテストに失敗することがなくなりました。めでたし、めでたし。
何故Windows環境ではテストに成功したのか
クラス生成で生じる差異
しかし、そもそも全く同じコードなのに、何故Widnows環境とUbuntu環境でテスト結果が異なるのでしょうか。これがDBD::mysqlやDBD::SQLiteなどに密接に関わったテストであればともかく、クラスの生成で差異が出るのは気持ち悪いところです。
ハッシュ関数の挙動ではなかろうか
テストが通った後によく考えてみた結果、これは完全に憶測なのですが、(ユーザーによる大文字PなPerlプログラムという意味ではなく、処理系としての小文字pな)perlのハッシュ関数の挙動が異なる所為ではないかと踏んでいます。
というのも、当の実装クラス側でbind_constructor関数に渡すargハッシュキーに該当する値はハッシュリファレンスになっています。ハッシュであるから、リストと違って順番は無視されます。
その順番は、ハッシュキーをハッシュ関数に通して得られるハッシュ値の順番なので、これはperlのエディションやバージョンによって異なるのはいかにもそれっぽいです。
そして、Windows環境でテストした際には、このハッシュ値の順が「たまたま」循環依存しない順番であったために、lazy_buildが奏功して依存の逆順でも動く処理となっていて、問題が生じなかったのではないだろうか......という推測です。つまり、たまたまWindowsだから免れたというのではなく、ハッシュキーの構成によってはWindows環境でも起き得たり、*nix環境では起き得なかったりする、と整理出来ます。
いずれにせよ注入順は肝になる
いずれにせよ、たまたま動いたからといってクラスをその名称でソートしてinject_classに突っ込んだのは、大いに反省するべきところです。実際、MooseX::OrochiのPODでも、ANNOTATIONS WITH MooseX::Orochi節にある通りinject_classの順番に気を使っていることが分かります。
$c->inject_class($_) for qw(
MyApp::Logger
MyApp::Schema
MyApp::Model::Foo
MyApp
);
私の場合、それに気付いてはいたのですが見逃していたという次第で、ただ単に気付かないよりも問題の根が深く、忸怩たる思いです。
use Module; 宣言を書く時、全部ABC順にしないとイライラする。
とまで、ソースが整然であることを求道していた牧さんが、何故敢えてそこでクラスのソートをしなかったのか、PODを読んで自ずと理解することを求められているような気がしました。
余談: やはりMooseの型は偉大だ
余談ですが、こうした問題を出来るだけ早期に発見するためにも、isaやdoesは満遍なく指定しておきたいものです。
確かに、Shawn M Mooreさん(sartakさん)は、性能面に鑑みると型を頻繁には使わないことをYAPC::Asia特別研修で勧めていらっしゃいました。しかし、私のような技術不足の人間は、本番環境ではともかく、少なくても開発環境で一通りのものが動くまでは、転ばぬ先の杖として、また文書化の一環としても、是非指定しておきたいと思うのです。無論、本番環境で当該部分をコメントアウトする折衷案はあり得ることです。
コメントする