単数選択ロジックを抽象化してみた

後日記:単数選択ならこんな複雑な実装は必要ない。各インスタンスに選択されているかどうかのフラグを持たせる必要などなく、、_active_instanceの記憶保持のみで、「定数時間」要件はいずれも実現できる。任意のインスタンスが選択状態かどうかは、_active_instanceとそのインスタンスとの一致を調べればよいだけなはず。

やあ子どもたち。元気でやっているか。
UIベースのソフトウェアツールで、複数のアイテムが表示されているときに、その中の一つまたは幾つかが「選択状態」になったり、「ハイライト表示」になったり、「アクティブ」になったり、あるいはそれがキャンセルされたりすることがよくありますよね。「アクティブ」は、「アクティブなモード」とか「アクティブなツール」とか言ったりもするので、UIや描画に限った話ではないかも知れません。とにかく、、これひっくるめて考えると何でしょう。
そう!要は「単数/複数選択」機能ですね。あるいは英語で「Single-Select/Multi-Select」といった方がしっくり来るかも知れない。今回はこのうち、「単数選択」のしくみを手軽に実装できる便利クラスの紹介です。なぜ、「単数選択(シングルセレクト(SingleSelect))」という、一見簡単な機構にわざわざ焦点を当てるのか、その答えは「定数時間」というキーワードにあります。以下、簡単のため、上述の「選択状態」を例に、話をします。
■機能要件
プログラム的な機能要件としては以下の2つになるかと

  • 今選択されている要素が定数時間で取得できること。何も選択されていなければ、それはそれで、その旨報告すること(null値を返すなどし)。
  • 任意の要素について、その要素が今選択されているのかどうかが定数時間で取得できること。

はい。いずれの要件にも出てくる、「定数時間」というキーワード。これが大事なポイントですね。定数時間、つまり、選択対象が何万個何億個数にのぼろうとも、一瞬で、結果を得たい、というわけです。だからこそ、ここに特別な決まりきった定番ロジックが存在する余地が生まれるのです。以下ではこれを実現するためのデータ構造とロジックを考えてみましょう。
■データ構造
実装方法にもよりますが、この場合、素直に考えて、

  • インスタンスごとに、「選択・非選択」いずれか1つの値をとる状態フラグ変数を1つもたせる
  • さらに全体管理処理のどこかで、「選択」状態にあるインスタンスへのポインタまたはIDを保持する

という設計にしましょう。
■処理ロジック
すると、この上記データ構造を運用管理するための処理ロジックとしては、以下の要件を満たすものが最低限必要になります。

  • あるインスタンスが「選択」された途端に、それまで「選択」状態にあったインスタンスは「非選択」状態にするべく、それぞれのインスタンスのフラグを正しく更新。
    ここで、さらに詳細として、
    • 「何も選択されていない状態」にしたい場合もある
    • 「何も選択されていなかった」場合もある。
    • 「すでに選択されているものが選択される」場合もある。

どうでしょうシングル・セレクト機能とはいえなかなか複雑ですね。しかしながらこういった複雑だが決まりきった処理が、冒頭で述べた「選択・非選択」「ハイライト表示・通常表示」「アクティブ・非アクティブ」といったありがちな「単数選択」機能要件を実装しようとするときに毎回必要となります。
そこでこの決まりきった処理を汎用性の高いテンプレートクラスとして考えました。それは以下のようなものです。

    template< class T >
    class ActiveInstanceManager
    {
    public:
        ActiveInstanceManager( void )
        :_active_instance( 0 )
        {}
        template< class F >
        void setActiveInstance( T t, F funcSetIsActive )
        {
            if( _active_instance == t )
                return;
            if( t )
                funcSetIsActive( t, !0 );
            if( _active_instance )
                funcSetIsActive( _active_instance, 0 );
            _active_instance = t;
        }
        T _active_instance;
    };

はい。でもこれはちょっとナイーブな実装です。いろいろやっていくと、funcSetIsActive の中で、本関数が再帰的に呼ばれ得る場合があります。具体的な例えを挙げると、ある要素がアクティブになった場合は、ケースに応じて別の要素をアクティブにする、または非アクティブにしたい場合などです。上記コードだと、初回呼び出しでされるべき_active_instanceの更新がされる前に、再帰の2回目呼び出しに行ってしまいます。そこで改良版が以下になります。

    // funcSetIsActive()の中で本関数が再帰的に
    // 呼ばれうる場合への対処バージョン
    template< class T >
    class ActiveInstanceManager
    {
    public:
        ActiveInstanceManager( void )
        :_active_instance( 0 )
        {}
        template< class F >
        void setActiveInstance( T t, F funcSetIsActive )
        {
            if( _active_instance == t )
                return;
            auto tmp = _active_instance;
            _active_instance = t;
            if( tmp )
                funcSetIsActive( tmp, 0 );
            if( t )
                funcSetIsActive( t, !0 );
        }
        T _active_instance;
    };

上記の利用者は、まず管理処理部分(UIから一歩離れたところに位置づけられるコード)にて上記クラスのインスタンスを作成し、保持しておきます。そして「アクティブ・非アクティブ」が切り替わる箇所のコード(よりUIに近いところのコード)で、本クラスの、setActiveInstance()を呼び出します。このとき、対象のインスタンスを最初の引数に、そして、インスタンスの状態フラグ変数を変更するためのメソッドを、「関数オブジェクト」という形で、2番めの引数に指定します。例えば以下の様に呼び出します。

// 管理処理部分のデータ構造の中で以下を作成・保持。
ActiveInstanceManager< Item* > _hilighted_mgr;
//
// UI近辺コードにおける、インスタンスの状態フラグが変化し、
// 描画を更新したいところで、以下を記述
//(ハイライト表示にするインスタンスが hit_itemである場合)
awesome_core->_hilighted_mgr.setActiveInstance
( 
  hit_item, 
  std::mem_fn( &Item::setIsHilighted )
);

(何もアクティブにしない場合は、setActiveInstanceには、nullptrを指定します。)
以上活用することで、冒頭に述べたような、決まりきった処理を毎回個別に記述しなくてはならない事態を回避することができるようになるはずです。
(なお、アクティブインスタンスの実際の取得と更新にあたっては、このままだと記述が多くなりすぎなので、ActiveInstanceManagerインスタンスを保持しているクラスに、ラッパーメソッドを用意した方がいいと思う。(例:setHilightedModel, getHilightedModelなど。。)

やー、抽象化しましたねぇ。各インスタンスのフラグをセットするメソッドの中で、単にフラグをセットするのみでなく、フラグの値で条件分岐していろいろ処理を書いたりすると、選択・非選択の状態管理のみならず、いろいろと幅広く使えたりするんですよこれ。なんていう謎の言葉を残しつつ。。
じゃ今日はこのへんで。チャオ!