2004-04-29
近況
"Building Application Frameworks" は挫折. 多数の著者によって各節が独立して書かれており, 記述に重複が多い. だから厚さほどの情報量はない. 内容を選り分ける気力が尽きた. トピックが独立だから, 中断して内容を忘れかけてもあまり害はあるまい. また気がむいたら読もう.
ある派閥争いの話
まわりの同僚達にはフレームワーク派とライブラリ派がいる. 以前, 同僚 B とそんな話をした. 利用者にインターフェイス(Java の interface/C の コールバック関数)の定義を要求するのがフレームワーク派, 自分のモジュールを呼出させるのがライブラリ派. 適切な設計は状況によって異り, 単純にこのどちらが優れているということはない. だからフレームワーク派/ライブラリ派という区分は意思決定での閾値が持つ傾向をあらわすと言える. 二つの派閥はよくもめる. そんなとき感情的になるのを避けるため, フレームワーク形式とライブラリ形式のどちらをとるかの判断基準ついて意識することには意味がある.
ライブラリ形式が必要な時
ライブラリ形式 (= 利用者が機能を呼びだす形式) が必要なのは, 利用者の望むタイミング(大抵はライブラリを呼びだした瞬間)に結果が欲しいケースだ. たとえば "レポジトリの値を読む" 操作, あるいは各種数値計算. ここでいう結果には副作用も含まれる: 例えば画面への描画, ウィンドウを開く/閉じる, データベースの値を書換えるなどは副作用にあたる.
フレームワーク形式が必要な時
フレームワーク形式 (= 利用者の登録した処理が呼びだされる形式) が必要なのは, 利用者がタイミングを決定できない時だ. 例えば CGI や servlet は Web ブラウザからアクセスされた時に処理を実行する. そのタイミングは当然 CGI 作成者には判断できない. だから (自分で HTTP Server を作らない限り) Web application は多少なりともフレームワークっぽくなる. main() を OS や JVM というフレームワークで定義された interface だと考えれば, これもタイミングを知ることのできない処理にあたる.
非同期と同期
こうしてタイミングに着目すると, ライブラリ形式は同期処理, フレームワーク形式は非同期処理に向いていることがわかる. だから同じ機能を提供するのにも同期性の問題から異なる形式をとることがある. たとえばネットワーク上のストリーム (HTTP など) は読み込みに時間がかかり, 処理がブロックするかもしれない.
処理がブロック(同期)していいならライブラリ形式をとれる.
...
while (true) {
byte[] buf = httpReader.read();
if (null == buf) {
break;
} else {
/* parsing stream ... */
}
}
...
非同期での読み出しが必要ならフレームワーク形式になる.
class MyHTTPStreamHandler implements HTTPStreamHandler {
...
void read( byte[] buf ) { // the signature is defined in HTTPStreamHandler
/* parsing stream ... */
}
}
...
void foo() {
...
HttpStreamReader reader = new HTTPStreamReader();
reader.read( "http://www.example.com", 80, new MyHTTPStreamHandler( ... ) );
...
}
このように, タイミング/同期性は形式を要請する. だからその要請がはっきりしている時はあまりもめずに設計が決まることが多い. メソッド呼出しがブロックしないと仮定すればライブラリ形式を貫きとおすことはできるし, 同期処理にフレームワーク形式を使うこともできる. しかしそのどちらどちらも利用者にとって煩雑. 一般的ではない.
どちらでもなんとかなる時
ライブラリ形式とフレームワーキ形式のどちらが望ましいかが上のようにすっぱり決まらないケースもある. XML パーサの SAX と XML Pull がその良い例だ. 同じ機能(XMLJのパース)を実現するのに, フレームワーク形式(SAX)とライブラリ形式(Pull)の両方があり, どちらにも支持者がいる. ここで問題になっているのは複雑だ; SAX と Pull のどちらを使うのがより簡単なのだろうか.
まず, フレームワーク形式とライブラリ形式を交換する方法について確認. 単純化していうと, ライブラリによって取得できたデータをコールバックの引数に渡せばフレームワークになる. 逆にフレームワークのコールバックに渡る引数をオブジェクトに詰めてモジュール呼出しからの戻り値とすればライブラリになる. (その値はコンテクストに保存されてもいい. Pull は parser がコンテクストとして値を保持し, メソッドでそれをとりだす.) SAX/Pull の場合を例にとろう.
SAX では, 要素の開始を次のメソッドに通知する. Attributes は属性の配列だと思えばいい.
public void startElement(java.lang.String uri,
java.lang.String localName,
java.lang.String qName,
Attributes atts) throws SAXException;
Pull では パーサの next() メソッドで次の token を読み出し, その種別に応じて 必要な情報をパーサから読み出す. parser は コンテクストとして, getName() など現在のトークン情報を取得するためのメソッドを持つ.
int eventType = parser.getEventType();
while (eventType != parser.END_DOCUMENT) {
if(eventType == parser.START_DOCUMENT) {
...
} else if(eventType == parser.END_DOCUMENT) {
...
} else if(eventType == parser.START_TAG) {
... /* we handle elements here in many cases. */
} else if(eventType == parser.END_TAG) {
...
} else if(eventType == parser.TEXT) {
...
}
eventType = parser.next();
}
"引数 - 戻り値" の変換ルールを使うことで書き換えることのできるものは多い. どちらが単純か, 拡張性があるか, 柔軟か. その判断は様々な要因に左右される. 設計者のセンスが問われる, と言って済ますこともできる. ただ, 判断に影響を与える要素をいくらかでも知っていればセンスに頼らない議論も少しはできるかもしれない.
気が向いたらつづく.