「DI(依存性注入)からどこへ行こうか その1」の直接の続きではない(つまり、「その2」ではない)のですが、関連した話題です。
ショー君に、Xion処理系(http://www.chimaira.org/tmp/Xion-0.1.tgz)のコンポネント化をやってもらいました。当然ながら、疑問点/問題点が出るわけで、それをケーススタディとして取り上げます。
内容:
話題、前提、注意
ショー君が挙げていた疑問/問題のなかで、例外インターフェースの設計法であるとか、抽象化レベルの問題だとかは、もっともな指摘なのですが後回しにします(面倒だから)。今日のところはもう少し説明しやすいネタ -- プログラムコードのどの部分をどこまでコンポネント化するか? といったハナシ。
Xion処理系は、XionParser(構文解析→Xion/Java:チュートリアル原案 その1 - 檜山正幸のキマイラ飼育記 (はてなBlog))、XionLoader(オブジェクトモデルに従ったオブジェクト構造の構築)、XionRealizer(ユーザー定義のオブジェクト構造の構築)の3つの利用形態があります -- XML処理を知っているなら、XionParserはSAXリーダーに、XionLoaderはDOMパーザーに似ている、と言えば見当は付くでしょう。
それで、主にXionParser(SAX風構文解析系)を、コンポネントの組み合わせとして再構成してみます。利用者からの見え方はほとんど変わらないので、一種のリファクタリングだと思ってよいでしょう。
コンポネント接続機構であるポートの実現には、フィールド・インジェクションを使います。フィールド・インジェクションに欠点があるのは承知ですが、とにかく単純明解ですからね(実用には、セッター・インジェクションがお勧めです)。
説明を煩雑<はんざつ>にしないために、肝心の例外/エラー処理をはしょってます。いずれ、たぶん、ひょっとするといずれチャント説明するよ。
絵の描き方
ポートベース・コンポネント(port-based components)は、オス/メスのポート(接続端子)が突き出た箱で表現します。ポートの実体は、オスは@Provide
、メスは@Require
とアノテートされたフィールドです。
「箱の左側が利用者から見える面」、「箱の右側がライブラリ/プラットフォームと接続する面」というのが原則ですが、ポートを右側から左側、左側から右側に移すのは自由です。よって、同一のコンポネント実装に対して、いくつかのポート配置がありえます。オス1本、メス1本のポートなら、配置のバリエーションは4種。
補足 - 蛇足
多くの人にとってはどうでもいいことでしょうが、僕には面白くてたまらないことなので、書いておきます。
上の図のメスに「A」, オスに「B」というラベルを張ること(レディファースト・ラベリング(ニコ))にして。左側ではオスに印「¬」を付け、右側ではメスに印「¬」を付けてオス/メスの区別をすることにします。すると、図の4種のバリエーションは、記号的に次のように表してよいでしょう。
- A, ¬B ⇒
- A ⇒ B
- ¬B ⇒ ¬A
- ⇒ ¬A, B
これは、論理学のシーケント計算(sequent calculus)における否定(¬は否定の記号です)の扱いとまったく同じです。この類似は偶然やコジツケではありません。
ポートベース・コンポネント達の組み合わせ計算は、コンパクト閉圏によって解釈できます(そういう意味論を構成できる)。一方、コンパクト閉圏は、乗法的線形論理(multiplicative linear logic)の簡略版を解釈する場にもなっています。結果的に、線形論理風のシーケント計算が、コンポネントの計算とうまく対応するわけです。ウヒョヒョヒョヒョ(謎の笑い)。
2つのコンポネント(箱)が結合できたときは、ワイヤーを描かずに、ピタッと箱をくっつけてしまいましょう -- レゴ方式の図示。レイアウトの都合で、箱から出ているポート(の棒)の長さは適当に伸縮させてもかまいません。
事例:Lexerのコンポネント化
Xionのレクサー(字句処理系)は、もともとが内部的なモジュールで、公開する意図はなかったので、パーザーとひっついています。が、これも部品化しておきましょう。現状のインターフェースは(Javaのinterfaceが在るわけではないけど):
XionParser.Token yylex() throws XionParseException, その他の例外
まず、yylexって名前は実装(に使ったライブラリ)が透けて見えてイヤだね、nextTokenとか、かな。
戻り値も例外もXionParser側で定義されているので、レクサーをひとり立ち(パーザーから独立)させるために、戻り値/例外を自前で定義しましょう。しかし、Token型と例外を汎用化するのは難しく、これは結局XionToken, XionLexExceptionとかになります。
public interface XionLexer { public XionToken nextToken() throws XionLexException, その他の例外; }
補足 - 蛇足
さほどのメリットがないから無理に抽象化しなくてもいいですが、型パラメータ(ジェネリックス)を使えば:
public interface Lexer<TokenT> { public TokenT nextToken() throws LexException<TokenT>, その他の例外; }
Lexer<XionToken> が XionLexerってこと。
入力ストリームはメス・ポートにより受け取ることにすると、レクサーのプロファイル(ポート群の仕様)と絵は次のよう。ポートが箱のどっち側に出ているかは、便宜上のことで、後から配置を変更してもいいです。
@Provide public XionLexer lexer; @Require public Reader input;
事例:Parserのコンポネント化
現状(つうか過去というか)のパーザー(構文解析系)のインターフェースは:
public void setExpressionHandler(XionExpressionHandler handler) public void parse(Reader input) throws IOException, XionParseException, XionHandleException;
parseの引数であるinputは、実はレクサーに渡されるもので、レクサーを別に切り出してしてしまった今は不要ですね。それで例えば、提供(@Provide
)するポートのインターフェースは:
public interface XionParser { public void setExpressionHandler(XionExpressionHandler handler) public void parse() throws IOException, XionParseException, XionHandleException; }
しかし、setExpressionHandlerの役割は、セッター・インジェクションによるインジェクターですから、XionExpressionHandlerの割り当てはメス・ポートにすべきでしょう。parseの例外は特に考慮しない(このままでいい)ことにすれば:
public interface XionParser { public void parse() throws IOException, XionParseException, XionHandleException; }
と、メソッド1個になってしまいました。
この例はやや極端でしたが、一般論として:
- インターフェースに含まれるメソッド群をポートとして切り出すべきか迷うことがある。そのメソッドがインジェクション目的ならメス・ポートにする。
- ポートのインターフェースが、独立した複数のコンポンネントから利用可能な部分に分割できるときは、2つの(あるいはもっと多くの)ポートにする。
雰囲気を絵に描くと:
それで結局、パーザー・コンポネントのプロファイルは:
@Provide public XionParser parser; @Require public XionLexer lexer; @Require public XionExpressionHandler handler;
組み立て
パーザーの組み立てを絵で描くと:
([追記 date="2006-09-20"]アラララ、名前が重複してました。Pで目印したパーザーコンポネントのparserポートの型がXionParser、最終的なプログラムの型もXionParserになっている、こりゃダメだ。適当にリネームして考えてください。「補足」を参照。[/追記])
一応、レクサー(文字Lが目印)のinputポートを右から左に移動してますが、これは概念的な変形で、ソフトウェア的な負担や影響はありません。
次に、レクサーとパーザー(文字Pが目印)をlexerポートで結合します(カチャッ)。左側に、input(メス), parser(オス), handler(メス)という3本のポートが出るので、適当なオブジェクト(仮に変数xで参照)で集約(アグリゲート)とラップをして、x.input, x.parser, x.handlerで各ポートにアクセスできるとしましょう。([追記 date="2006-09-20"]集約とラップの説明が不十分で、これじゃわかりませんね、でも、あんまり気にしなくていいです。「補足」を参照。[/追記])
絵でウニョウニョした雲形で描いてあるのは、従来のインターフェースに合わせるためのコードです。このウニョウニョは、だいたい次のよう。
public void setExpressionHandler(XionExpressionHandler handler) { x.handler = handler; } public void parse(Reader input) throws IOException, XionParseException, XionHandleException { x.input = input; x.parser.parse(); }
上の短いコードだけで、インターフェースのエミュレートはできます。もちろん、動作は変わりません。
グルーコード
図のウニョウニョ部分 -- つまり、実行時にコンポネントを接合したり、インターフェースを調整するためのコードを、僕はグルーコードと呼んでいます(「グルーコード」自体は割と一般的な用語)。
コンポネントの繋ぎ合わせ/組み立ては、設計段階で静的に行われるのが理想的でしょうが、それにこだわるのは得策とは思えません。箱(コンポネント)どおしがポート結合だけで(あたかもレゴブロックのように)カチャッ、ピタッと全部くっつけばいいのですが、形状が合わないときは、遠慮無く粘土とか糊とかで埋め合わせをしてしまう、という発想ですね。
最初はグルーコードを書いても、それを別コンポネントとしてまとめる、コンテナ機能に吸収させる、静的デザインツール側に取り込む、とかで、グルーイング(貼り合わせ)のハードコーディングを徐々に減らせればいいと思います。
XionLoaderはどうなる?
絵だけ示しておきます。Bで目印されたコンポネントはモデル・ビルダーです。XionExpressionHandlerを実装しており(つまり、オス・ポートが出ている)、構築した成果物は、インターフェースResultを通して取得できます。
public interface Result { /** なんらかの作業結果を返す */ public Object result(); /** result()を呼んで意味のある値を得られるかどうか */ public boolean isAvailable(); }
それで
Xion処理系は小さなライブラリですが、それでもオモチャというわけではありません(遊び道具という意味ではオモチャか)。この程度のプログラムなら、少量のグルーコードで完全なコンポネント化が可能です。
もっとも実は、Xion処理系は最初からコンポネント化する想定で作っているのでズルしていると言われても仕方ないですがね。ただ、ズルしてるのは僕だけで、ショー君はむしろ、インターフェースを使わないモノリシックな作りを意図してたはずです(僕が「そうしろ」と言っていたので)。
実験としては小規模かつ特殊ケースなので、一般的な物言いはできませんが、使えそうな気がしないでもない。現実的な問題の対処法は、また追って報告と説明をするでしょう(たぶん)。