Super Technique 講座

ザ・レトロ・アルゴリズム「コントロールブレーク」by OOP

以前書いた「バイナリサーチ」徹底解説の続編みたいなものである。筆者はこのところ、業務プログラムを書き倒しているのだが、その時に結構頻繁に遭遇するちょっとした問題を解決するにあたって、同僚たちがあまりうまいやり方を知らない....というのを、少し気にしているのである。その問題とはこういうタイプのものだ。

  1. データベースのあるテーブルが、実際には階層構造になっているにも関わらず、フラットに定義されているために、実際に欲しい階層データを構築するのに何回も SQL を呼んでいる....サブキーを考慮して order by してやれば、正しい順番で1回で取れるのにねぇ...
  2. そういうデータで、階層ごとに合計が欲しいんだが、Map を使って合計やってるぞ...これも Map とか使わずに、1回ループさせるだけでできるのにね....

という問題だ。これ要するに、階層データがベタな SQL のテーブル1個で定義されているので、「フラットなデータを階層的に扱う」というのがどうも困惑のタネになっているようなのである。まあ、こういうDB設計はテーブルの数を節約する、というメリットがあって採用されている(あと判りやすいしね)わけなんだが、どうもそのハンドリングで「やり方を知らない...」分、余計な手間をかけているケースが目に付くわけだ。勿論、SQL の発行回数は少なければ少ないほど効率がいいのは当たり前だ。特に「スピードをもう少し速くしたいよね...」というのは、要望としても無視できるものではない。そういうあたりで、筆者なぞは「そりゃコントロールブレークじゃん!」となるのは、それは筆者がレトロなせいだ。

まあ、コントロールブレークなるものは、その昔に「プログラミング≒COBOL」という概念だった時代からあるもので、基本情報技術者試験で受験者をイジメるためにあるような処理だったオボエが筆者なぞはある。とはいえ、ここらへんの技術(バイナリサーチもそうだったが)は、イマドキのプログラマの教養からはスッポリ抜け落ちているような雰囲気だ。というわけで、こんな話をするのも良かろう...と思うのだが、勿論このページはイマドキの話なので、このコントロールブレークを「オブジェクト指向でやっちゃおう」というのがネタである。そういう汎用ライブラリも書いたので、ぜひぜひ活用してくれたまえ。あ、元々この「スーパーテクニック講座」はCで書くのが原則なんだが、このページでは Java で話をする。OOP話だからそういうことでいいな。

で細かいことは「俺はどーでもいーぞ」という向きは、さっさと Java で実装した筆者特製汎用コントロールブレークライブラリを落として、ソースと JavaDoc を見て使ってみてくれたまえ。どうせこんなんPDSでいいぞ。

目次

問題

で問題のデータはこういうタイプのものだ。

package cb;

import java.util.List;
import java.util.ArrayList;

class ShopData {
    private String areaName;
    private String prefName;
    private String shopName;
    private int value;

    public String getAreaName() { return areaName; }
    public String getPrefName() { return prefName; }
    public String getShopName() { return shopName; }
    public int getValue() { return value; }

    ShopData( String l1, String l2, String l3, int v ) {
	areaName = l1;
	prefName = l2;
	shopName = l3;
	value = v;
    }

    public String toString() {
	return areaName + ":" + prefName + ":" + shopName + ":" + value;
    }

    /* 思いっきり単純にサンプルデータを得るためのメソッド(苦笑) */
    public static List<ShopData> getExample() {
	List<ShopData> srcs = new ArrayList<ShopData>();
    
	srcs.add( new ShopData( "関東", "東京", "シナガワ", 1000 ) );
	srcs.add( new ShopData( "関東", "東京", "シブヤ", 2000 ) );
	srcs.add( new ShopData( "関東", "東京", "シンジュク", 3000 ) );
	srcs.add( new ShopData( "関東", "東京", "シンジュク", 4000 ) );
	srcs.add( new ShopData( "関東", "横浜", "ツルミ", 1100 ) );
	srcs.add( new ShopData( "関東", "横浜", "ヨコハマ", 1200 ) );
	srcs.add( new ShopData( "中部", "名古屋", "ナゴヤ", 2300 ) );
	srcs.add( new ShopData( "関西", "大阪", "ウメダ", 3100 ) );
	srcs.add( new ShopData( "関西", "大阪", "ウメダ", 1021 ) );
	srcs.add( new ShopData( "関西", "大阪", "ツルハシ", 3200 ) );
	srcs.add( new ShopData( "関西", "大阪", "ナンバ", 3300 ) );
	srcs.add( new ShopData( "関西", "京都", "カワラマチ", 4400 ) );
	srcs.add( new ShopData( "関西", "京都", "サンジョウ", 4500 ) );
	srcs.add( new ShopData( "関西", "神戸", "コウベ", 5100 ) );
	srcs.add( new ShopData( "関西", "奈良", "ナラ", 6100 ) );
	srcs.add( new ShopData( "九州", "福岡", "ハカタ", 1010 ) );
	srcs.add( new ShopData( "九州", "福岡", "フクオカ", 1020 ) );
	srcs.add( new ShopData( "九州", "福岡", "フクオカ", 1011 ) );
	return srcs;
    }
}

まあ要するに、全国ネットの営業所がある会社のほにゃららな売り上げ処理をする、という格好のものが、イメージしやすいだろう。DBだったらこんな定義だな。

CREATE TABLE shop_data (
   id INTEGER,
   areaName VARCHAR(16),
   prefName VARCHAR(16),
   shopName VARCHAR(32),
   value INTEGER );

で、各支店ごとのリストを得た上で、各エリア、各県、各支店ごとの売り上げ計を集計する、ということがしたいわけである。勿論コレを、

SELECT DISTINCT areaName from shop_data;
SELECT DISTINCT areaName,prefName from shop_data;
SELECT DISTINCT areaName,prefName,shopName from shop_data;

とかやって、たとえば、

package cb;

import java.util.List;
import java.util.ArrayList;

public class ResultGroup {
    private Object key;
    public Object getKey() { return key; }
    public void setKey( Object v ) { key = v; }

    private List childs = new ArrayList();
    public List getChilds() {
	return childs;
    }
}

のような階層データを構築するんじゃ、たまったものではない。実はこれ、

SELECT * FROM shop_data ORDER BY areaName,prefName,shopName;

のSQLを一発発行するだけで、全データを読み込んで、しかもループ1回だけで階層構造を作っちゃうこと(同時にエリア別、県別、支店別集計も....)ができるのである!

COBOL流の実装(非OOP)

まあ、基本情報技術者試験(COBOL)とかのテキストを見ると、こういうタイプの問題は、「複数キーによるグループトータル」とかいう見出しでやり方が載っている。

東西商事の売り上げデータを読み、支店別の売り上げ報告書を作成しなさい。
▼処理条件
(1) 売上ファイルは地区・販売店・商品コードを基準に昇順で整列されている。
(2) 商品区分は5区分あり....(これはどうでもいい)
(3) 一件のデータを読み、金額を計算する(これもどうでもいい)
(4) 商品コード・販売店コード・地区コードが変わったところでそれぞれの合計を印字する。
(5) 最後に全国の売り上げ合計を印字して処理が終わる。

冗談として、そのコードを記述する....のは悪趣味なので止めておくが、これを何も考えずに Java で書き直すと、こんな感じになる。勿論この問題はCOBOLらしく「印字する」で結果出力なので、そういうレベルでまず実装する。

package cb;

import java.util.List;
import java.util.Stack;

public class ControlBreakOfCobol {
    public static void main( String [] args ) {
	List example = ShopData.getExample();
	ControlBreakOfCobol cobol = new ControlBreakOfCobol();
	cobol.build( example );
    }

    // 第0レベルの担当
    public void build( List in ) {
        // この PeekableIterator というのがマジックの種。
	PeekableIterator iterator = new PeekableIterator( in );
	int total = 0;
	while( iterator.hasNext() ) {
	    total += area( iterator );
	} 
	System.out.println( "全国総計=" + total );
    }

    // 第1レベルの担当
    private int area( PeekableIterator i ) {
	int total = 0;
	ShopData src = (ShopData)i.peek(); // まず peek() で読む
	Object key = src.getAreaName();  // 第1レベルのキーを保持
	while( true ) {
	    total += pref( i, key );
	    if( i.hasNext() ) {
		src = i.peek();
		// キーが変われば、このレベルはおしまい
		if( key.equals(src.getAreaName()) == false ) {
		    break;
		}
	    } else {
		break;
	    }
	}
	System.out.println( "地域:" + key + "=" + total );
	return total;
    }

    // 第2レベルの担当
    private int pref( PeekableIterator i, Object key1 ) {
	int total = 0;
	ShopData src = (ShopData)i.peek();
	Object key = src.getPrefName(); // 第2レベルのキーを保持
	while( true ) {
	    total += shop( i, key1, key );
	    if( i.hasNext() ) {
		src = i.peek();
		// キーが変われば、このレベルはおしまいなのだが、
		// このキーが同じでも上位キーが変わればやはりレベルが変わる。
		// だから、上位キーを探索できないといけない。
		if( key.equals(src.getPrefName()) == false || 
		    key1.equals(src.getAreaName()) == false ) {
		    break;
		}
	    } else {
		break;
	    }
	}
	System.out.println( "県:" + key + "=" + total );
	return total;
    }

    // 第3(末端)レベルの担当
    private int shop( PeekableIterator i, Object key1, Object key2 ) {
	int total = 0;
	ShopData src = (ShopData)i.peek();
	Object key = src.getShopName(); // 第3レベルのキーを保持
	while( true ) {
	    i.next(); // 次へ進むのは、末端レベルだけ
	    total += src.getValue();
	    // 末端レベルなので、ShopData を表示しておく
	    System.out.println(src);
	    if( i.hasNext() ) {
		src = i.peek();
		// キーが変われば、このレベルはおしまいなのだが、
		// このキーが同じでも上位キーが変わればやはりレベルが変わる。
		// べた書きはつらいよ...
		if( key.equals(src.getShopName()) == false || 
		    key2.equals(src.getPrefName()) == false ||
		    key1.equals(src.getAreaName()) == false ) {
		    break;
		}
	    } else {
		break;
	    }
	}
	System.out.println( "支店:" + key + "=" + total );
	return total;
    }
}

ここで使う、PeekableIterator ってクラスは、要するに「java.util.Iterator + peek()」という Iterator の拡張である。皆さん Iterator は使ってるだろうけども、筆者に言わせると、Iteratorの設計は?である。というのは、筆者は結構 Meyer 信者なもんで、

Iterator の next() が値を返すクセに副作用がある!

のが気に入らないのだ。勿論この副作用は、内部的にポインタを1つ進ませる、という重要な機能であることは言うまでもないのだが、

先読みみたいな感覚で、単にポインタ位置を参照するだけで、ポインタを動かさない next() が欲しい....

ということが結構あるのである。だから、そのために peek() というメソッドを追加して、「現在のポインタ位置のオブジェクトを返すが、ポインタは動かさない」という内容で実装したものである。要するにこのケースだと、一種の「先読み」みたいなことが必要になるので、一般的な Iterator だとキレイにロジックを書きにくい。ロジックの都合で Iterator を拡張するのはナンなのだが、こういう考え方もアリだろう。

package cb;

import java.util.List;
import java.util.Iterator;

public class PeekableIterator<T> implements Iterator<T> {
    private List<T> list;
    private int p;

    public PeekableIterator( List<T> l ) {
	list = l;
	p = 0;
    }

    public boolean hasNext() {
	if( p < list.size() ) {
	    return true;
	} else {
	    return false;
	}
    }

    public T next() {
	return list.get(p++);
    }
	
    public T peek() {
	return list.get(p);
    }

    public void remove() {
	/* NOP */
    }
}

とはいえ、この PeekableItarator の実装はあまり良くないので、配布版では PeekableIterator は interface になっている。その理由は....というと、list.get(int) をしているので、List が java.util.LinkedList だとメチャ効率が悪いんである。これはもう少しマトモに Builder にして、PeekableIteratorBuilder が適切な PeekableIterator を implements した内部クラスを返す、という風にすべきだな。配布版はそうしているので、これは参考だけだ。

でまあ、これで COBOL のレベルでの実装ができることになる。結果は各自で確認してくれたまえ。

非再帰オブジェクト構築版

しかし、ControlBreakOfCobol の実装は何だな、いっくら何でもOOPじゃないな。ShopData クラスと結び付いているので再利用性はゼロであるし、レベルがもう1レベル増えたらワシワシと修正しなければならない。また、出力を印字するだけなので、不便極まりないものだ。今からこれをOOPっぽく修正していく。

一般に「コントロールブレーク」と呼ばれる処理は、今見たように、キーの同一性が切れた時に、何か(この場合は合計印字)をさせる、というのが本来の意味だ。この概念は少しもオブジェクト指向ではない。これをオブジェクト指向っぽく言い直すと、

無構造なフラットなデータを、階層構造を持ったオブジェクトに変換する

というのが、ホントの「コントロールブレークの意味」なんだと筆者は思う。というわけで、まずはこれを、階層構造データに化けさせてみよう。合計処理だって、その階層データにさせればいいわけだ(デザパタ的には Composite だ)。まずはコレを 「再帰は使わない」レベルで実現しよう。

package cb;

import java.util.List;

public class CompositeOfCobol {
    public static void main( String [] args ) throws Exception {
	List example = ShopData.getExample();
	CompositeOfCobol builder = new CompositeOfCobol();
	builder.setResultClass( TotalResultGroup.class );
	ResultGroup ret = builder.build( example );
	System.out.println( ret );
    }

    // 生成クラスの実装を指定する
    private Class resultClass = DefaultResultGroup.class;
    public void setResultClass( Class clazz ) {
	resultClass = clazz;
    }

    // Composite の部品を指定されたクラスで生成する
    protected ResultGroup getResultInstance( Object key ) throws Exception {
	ResultGroup ret = (ResultGroup)resultClass.newInstance();
	ret.setKey( key );
	return ret;
    }

    public ResultGroup build( List in ) throws Exception {
	PeekableIterator iterator = new PeekableIterator( in );
	// Compositeのルートを作る
	ResultGroup root = getResultInstance("総計"); 
	while( iterator.hasNext() ) {
	    root.getChilds().add( area( iterator ) );
	} 
	return root;
    }

    private ResultGroup area( PeekableIterator i ) throws Exception {
	ShopData src = (ShopData)i.peek();
	Object key = src.getAreaName();
	ResultGroup ret = getResultInstance(key);
	while( true ) {
	    ret.getChilds().add( pref( i, key ) );
	    if( i.hasNext() ) {
		src = i.peek();
		if( key.equals(src.getAreaName()) == false ) {
		    break;
		}
	    } else {
		break;
	    }
	}
	return ret;
    }

    private ResultGroup pref( PeekableIterator i, Object key1 ) throws Exception {
	ShopData src = (ShopData)i.peek();
	Object key = src.getPrefName();
	ResultGroup ret = getResultInstance(key);
	while( true ) {
	    ret.getChilds().add( shop( i, key1, key ) );
	    if( i.hasNext() ) {
		src = i.peek();
		if( key.equals(src.getPrefName()) == false || 
		    key1.equals(src.getAreaName()) == false ) {
		    break;
		}
	    } else {
		break;
	    }
	}
	return ret;
    }

    private ResultGroup shop( PeekableIterator i, Object key1, 
                              Object key2 ) throws Exception {
	ShopData src = (ShopData)i.peek();
	Object key = src.getShopName();
	ResultGroup ret = getResultInstance(key);
	while( true ) {
	    src = i.next();
	    ret.getChilds().add( src );
	    if( i.hasNext() ) {
		src = i.peek();
		if( key.equals(src.getShopName()) == false || 
		    key2.equals(src.getPrefName()) == false ||
		    key1.equals(src.getAreaName()) == false ) {
		    break;
		}
	    } else {
		break;
	    }
	}
	return ret;
    }
}

ここで、後々を睨んで少しJavaっぽいことをしている。build() の戻り値は ResultGroup だが、これは実は、

package cb;

import java.util.List;

public interface ResultGroup {
    List getChilds();
    void setKey( Object v );
    Object getKey();
}

というタダのインターフェイスだ。この実装クラスとして、

package cb;

import java.util.List;
import java.util.ArrayList;

public class DefaultResultGroup implements ResultGroup {
    private Object key;
    public Object getKey() { return key; }
    public void setKey( Object v ) { key = v; }

    private List childs = new ArrayList();
    public List getChilds() {
	return childs;
    }


    public String toString() {
	StringBuffer sb = new StringBuffer();
	for( int i = 0; i < childs.size(); i++ ) {
	    sb.append( childs.get(i).toString() + "\n" );
	}
	return sb.toString();
    }
}

があるが、今回合計も一緒にやりたいので、さらにこの継承クラスである、TotalResultGroup を使うことにして、

   CompositeOfCobol builder = new CompositeOfCobol();
   /* 結果を返すオブジェクトの実装クラスを指定する */
   builder.setResultClass( TotalResultGroup.class );
   ResultGroup ret = builder.build( example );
   /* ret はこの時、TotalResultGroup のインスタンスである */

ということになる。つまり、ResultGroup を implements した任意の戻り値クラスを、ここで使えるようにしてある。後は、ほぼ先ほどの ControlBreakOfCobol の変形に過ぎない。逆に言えば、コントロールブレークの変形から、「フラットなデータを階層化する」ができる、ということ自体が、

コントロールブレークとは、フラットなデータを階層化することである

とした先ほどのテーゼの証明みたいなものだな。

配布版

では、今度は CompositeOfCobol をも少しオブジェクト味にしてやろう。まず、ShopData 専用に書かれている部分を、やはり ControlBreakKey interface にしてやろう。この時、地域、県、支店の3レベルのキーがあったわけだが、これを抽象化して、

package cb;

public interface ControlBreakKey {
	Object getLeveledKey( int level );
}

引数の level に対応したキーを返すようにする。だから、これを ShopData が implements すると、

class ShopData implements ControlBreakKey {
    private String areaName;
    private String prefName;
    private String shopName;
    private int value;

    public String getAreaName() { return areaName; }
    // ゲッタ類略

    ShopData( String l1, String l2, String l3, int v ) {
    // コンストラクタ詳細略
    }

    // ControlBreakKey が求める
    public Object getLeveledKey( int level ) {
        /* level は 1 始まり。(0==トップレベルになる) */
	switch( level ) {
	case 1:
	    return areaName;
	case 2:
	    return prefName;
	case 3:
	    return shopName;
	default:
	    /* null を返すレベルよりは深く潜らない */
	    return null;
	}
    }

    // 後略

のような感じだ。もうレベル制限はないわけで、好きなだけ深いレベルのデータをこれで階層的に整理できるようになる。ではソースはこんなところだ(配布版)。

package jp.or.nurs.sug.cb;

import java.util.List;
import java.util.Stack;

/** コントロールブレークを行う汎用ビルダ
 * キーを指定するメソッドを備えた ControlBreakKey interface を implements したオブジェクトの
 * List を引数として build(List) に渡すと、キーに対応した Composite な ResultGroup
 * オブジェクトを構築して返す。
 * 戻り値の実装クラスは、setResultClass(Class) で与える。もし、これを呼ばずに build() 
 * を呼べば、DefaultResultGroup で構築する。
 * @author  K.Sugiura
 * @version 1.0, 2007.8.26
 * @since   1.0
 */
public class ControlBreakBuilder {
    private boolean debug = false;
    /** デバッグ用途。true がセットされればフックを呼ぶ時にデバッグ出力をする。
     * @param debug  デバッグフラグ
     */
    public void setDebug(boolean debug) { this.debug = debug; }

    private Class resultClass = DefaultResultGroup.class;
    /** ResultGroup を implements したクラスを
     * 与え、結果戻り値の Composite の実装クラスを指定する。
     * @param clazz 実装クラス
     */
    public void setResultClass( Class clazz ) {
        resultClass = clazz;
    }

    /** setResultClass() で与えられた ResultGroup implements クラスから、
     * インスタンスを生成する。上書き用フックである。
     * 特に独自の例外は投げないが、Class.newInstance() を呼んでオブジェクトを
     * 生成する都合上、java.lang.Exception を投げることにしている。
     * @param key レベルキー(==null ならトップレベル)
     * @return  生成オブジェクト
     */
    protected ResultGroup getResultInstance(Object key) throws Exception {
        ResultGroup ret= (ResultGroup)resultClass.newInstance();
        ret.setKey( key );
        return ret;
    }

    /** 引数 List のデータを、階層化して返す。
     *  特に独自の例外は投げないが、戻り値オブジェクトを生成するのに、
     * getResultInstance() を呼び、その中で Class.newInstance() が呼ばれて
     * オブジェクトを生成する都合上、java.lang.Exception を投げることにしている。
     * @param in 構築対象の List
     * @return  階層化された結果
     */
    public ResultGroup build( List in ) throws Exception {
        // Builder の PeekableIteratorBuilder で PeekableIterator を構築
        PeekableIterator iterator = new PeekableIteratorBuilder().build( in );
        ResultGroup top = getResultInstance(null);
        // 階層情報は Stack で渡すのが良かろう
        Stack<ResultGroup> resultStack = new Stack<ResultGroup>();
        resultStack.add( top );
        // 第0レベルに入った(フック)
        onLevel( 0, iterator, resultStack );
        while( iterator.hasNext() ) {
            proc( 1, iterator, resultStack );
        }
        // 第0レベル終了(フック)
        onBreak( 0, iterator, resultStack );
        return top;
    }

    // 再帰する下請けメイン
    private void proc( int level, PeekableIterator i, 
                       Stack<ResultGroup> resultStack ) throws Exception {
        ControlBreakKey at = (ControlBreakKey)i.peek();
        Object saveKey = at.getLeveledKey( level );
        List childs = resultStack.peek().getChilds();
        if( saveKey == null ) {
	    // 末端レベル処理
            i.next(); // 読み捨てて進ませる
            Object o = onAdd( level, i, resultStack, at );
            if( o != null ) {
                childs.add( o );
            }
            return;
        }

	// 以降は非末端処理
        ResultGroup newGroup = getResultInstance( saveKey );
        resultStack.push( newGroup );
	// 新レベルに入った
        onLevel( level, i, resultStack );

        try {
            while( true ) {
                // 再帰します	    
                proc( level + 1, i, resultStack );
                if( i.hasNext() ) {
                    ControlBreakKey cbk = (ControlBreakKey)i.peek();
                    // キー階層チェック
                    if( checkKeys( level, cbk, resultStack ) == false ) { 
                        return;
                    }
                } else {
                    return;
                }
            }
        } finally {  // 筆者この書き方好き。内部の return の後ここに来るぞ。
            // 担当レベル終了
            onBreak( level, i, resultStack );
            // これは最適化で、子がない Composite は追加しない
            if( newGroup.getChilds().size() > 0 ) {
                childs.add( newGroup );
            }
            resultStack.pop();
        }
    }

    // キー階層チェック。
    private boolean checkKeys( int level, ControlBreakKey cbk, 
                               Stack<ResultGroup> resultStack ) {
        // Stack のメリットあり
        for( int i = level; i > 0; i-- ) {
            Object key = cbk.getLeveledKey( i );
            ResultGroup at = resultStack.get(i);
            if( at.getKey().equals(key) == false ) {
                return false;
            }
        }
        return true;
    }

    /**
     *  階層キーを表示用に置換する。
     * @param resultStack  現在呼ばれている階層のスタック
     * @return  表示用階層名
     */
    protected String getKeys( Stack<ResultGroup> resultStack ) {
        String ret = "";
        String delim = "";
        for( int i = 1; i < resultStack.size(); i++ ) {
            ret += delim + resultStack.get(i).getKey().toString();
            delim = ">";
        }
        return ret;
    }

    /**
     * 新しいレベルに入った時に呼ばれるフック。onEnd() と対応している。
     * デフォルトでは何もしないが、setDebug(true) の影響を受ける。
     * @param level  階層レベル
     * @param i 未処理データの Iterator
     * @param resultStack  現在呼ばれている階層のスタック
     */
    protected void onLevel( int level, PeekableIterator i, 
                            Stack<ResultGroup> resultStack ) {
        if( debug ) {
            String pop = getKeys( resultStack );
            System.out.println( "onLevel(" + level + "): key=" + pop );
        }
    }

    /**
     * データを追加する時に呼ばれるフック。
     * デフォルトでは引数 cbk をそのまま返す。setDebug(true) の影響を受ける。
     * だから、ここで「追加すべきオブジェクト」を本当の追加すべきオブジェクト
     * に編集することができる。もし、戻り値が null ならば、追加は行われない。
     * @param level  階層レベル
     * @param i 未処理データの Iterator
     * @param resultStack  現在呼ばれている階層のスタック
     * @param cbk  追加すべきオブジェクト
     * @return 追加されるオブジェクト
     */
    protected Object onAdd( int level, PeekableIterator i, 
                            Stack<ResultGroup> resultStack,
                            ControlBreakKey cbk ) {
        if( debug ) {
            System.out.println( "onAdd(" + level + "):" + cbk.toString() );
        }
        return cbk;
    }

    /**
     * 現在のレベルから抜ける時に呼ばれるフック。onLevel() と対応している。
     * デフォルトでは何もしないが、setDebug(true) の影響を受ける。
     * @param level  階層レベル
     * @param i 未処理データの Iterator
     * @param resultStack  現在呼ばれている階層のスタック
     */
    protected void onBreak( int level, PeekableIterator i, 
                            Stack<ResultGroup> resultStack ) {
        if( debug ) {
            String pop = getKeys( resultStack );
            System.out.println( "onBreak(" + level + "): key=" + pop );
        }
    }
}

まあ、こういうコードが筆者の得意技の部類だ。他のページをお読みの方だと気がつくかもしれんが、筆者の

  1. 再帰好き
  2. スタック好き
  3. 先読み好き

の王道パターンのコードような気がしないでもない。まあ、ここでのトリックの中心は、階層データを Stack で渡す、というやり方だろうね。何かシゴト系のソースを見て、java.util.Stack 使ってるのがチーム内で筆者だけ....というアワワな事態に遭遇しているんだけど、筆者はコレ大好きだ。だって、セマンティクスが明快じゃん。push(), pop() をアトミックに「動作」として捉える、というのがイメージしやすいんだなぁ。だし、こっちはキッチリと peek() がある(万歳!)。

フックの活用例

まあ、問題は onLevel(), onAdd(), onEnd() の3つのフックの使い方だ。要するにこれ、単に setResultClass() で Composite オブジェクトの実装クラスを指定して、後で実装クラスの機能として何か面白いことをさせる...というだけではなくて、この ControlBreakBuilder の途中での「介入ポイント」を作っておいて、そこで何か面白いことをさせる...というのが狙いなのである。

たとえば、ResultGroup 実装クラスが、

package jp.or.nurs.sug.cb;

import java.util.List;
import java.util.ArrayList;

public class SumupResultGroup extends DefaultResultGroup {
    private int total;
    public void setTotal( int v ) {
	total = v;
    }
    public void addTotal( int v ) {
	total += v;
    }
    public int getTotal() {
	return total;
    }

    public String toString() {
	StringBuffer sb = new StringBuffer();
	List childs = getChilds();
	for( int i = 0; i < childs.size(); i++ ) {
	    if( childs.get(i) instanceof SumupResultGroup ) {
		sb.append( childs.get(i).toString() );
	    }
	}
	Object key = getKey();
	key = (key==null) ? "総計" : key;
	sb.append( key + " total: " + getTotal() + "\n" );
	return sb.toString();
    }
}

というようなもので、これを使って、「ControlBreakBuilder が List をスキャンするのと同時に、総計/地域計/県別計/支店別計をすべて計算し、そのレベルの total に入れちゃおう」という、一番効率のいい SumupControlBreakBuilder を実装してみよう。

package jp.or.nurs.sug.cb;

import java.util.List;
import java.util.Stack;

/** コントロールブレーク+合計を行うビルダ。
 * ControlBreakBuilder の上書き例として作成した。
 * フックはこういう風に書く、という例である。
 * 対象List のオブジェクトは ControlBreakKeyAndValue
 * を実装する必要がある。
 * 戻り値の実装クラスは SumupResultGroup に固定されている。
 * @author  K.Sugiura
 * @version 1.0, 2007.8.26
 * @since   1.0
 */
public class SumupControlBreakBuilder extends ControlBreakBuilder {
    private Class sumupResultClass = SumupResultGroup.class;
        
    /** ResultGroup のインスタンスを生成する。このクラスでは、SumupResultGroup で固定されている。
     * @param key レベルキー(==null ならトップレベル)
     * @return  生成オブジェクト(SumupResultGroup固定)
     */
    protected final ResultGroup getResultInstance(Object key) 
                                                    throws Exception {
        ResultGroup ret= (ResultGroup)sumupResultClass.newInstance();
        ret.setKey( key );
        return ret;
    }

    /**
     * 新しいレベルに入った時に呼ばれるフック。onEnd() と対応している。
     * このクラスでは、自身のレベルの総計の初期化を行う。
     * @param level  階層レベル
     * @param i 未処理データの Iterator
     * @param resultStack  現在呼ばれている階層のスタック
     */
    protected void onLevel( int level, PeekableIterator i, 
                            Stack<ResultGroup> resultStack ) {
        SumupResultGroup at = (SumupResultGroup)resultStack.peek();
        at.setTotal( 0 );
    }

    /**
     * データを追加する時に呼ばれるフック。
     * このクラスでは、対象オブジェクト(cbk)の値を取得し、階層を遡ってその値
     * を合計に追加する。だから ControlBreakKeyAndValue 
     * を対象オブジェクトとする。
     * @param level  階層レベル
     * @param i 未処理データの Iterator
     * @param resultStack  現在呼ばれている階層のスタック
     * @param cbk  追加すべきオブジェクト
     * @return 追加されるオブジェクト(==cbk)
     */
    protected Object onAdd( int level, PeekableIterator i, 
                          Stack<ResultGroup> resultStack,
                          ControlBreakKey cbk ) {
        int value  = ((ControlBreakKeyAndValue)cbk).getValue();
	// 遡る...
        for( int j = 0; j < level; j++ ) {
            SumupResultGroup at = (SumupResultGroup)resultStack.get(j);
            at.addTotal( value );
        }
        return cbk;
    }

    /**
     * 現在のレベルから抜ける時に呼ばれるフック。onLevel() と対応している。
     * このクラスでは何もしない。
     * @param level  階層レベル
     * @param i 未処理データの Iterator
     * @param resultStack  現在呼ばれている階層のスタック
     */
    protected void onBreak( int level, PeekableIterator i, 
                            Stack<ResultGroup> resultStack ) {
    }
}

というわけで、新たに getValue() をインターフェイスとして定めた ControlBreakKeyWithValue interface が必要となるが、これくらいは折込み済みだろう。

こんな感じで、何かしたいことがあれば、ResultGroup 継承クラスか、ControlBreakBuilder を継承したクラスでフックを実装してやるくらいなことで、ややしく間違いやすいコントロールブレークを簡単に処理できちゃうのである。ぜひぜひお使いあれ。



copyright by K.Sugiura, 1996-2006