Buddy イディオム(仮)

長い間、どう実装すべきか悩んでいた問題をうまく解決できました。とはいっても、オブジェクト指向に馴染んだ人には当たり前すぎることかもしれないし、派手なトリックもありません。ただ、個人的にはとても大きな気付きがあって嬉しかったので、記念に書いておきます。

問題

新エンジンには、見出し語を構築する SKKEditor というコンポーネントがあります。主なインタフェースは文字の入力(とローマ字かな変換)、削除、カーソル移動などです。

class SKKEditor {
    //...
public:
    //...

    // 文字種変更
    void SelectInputMode(SKK::InputMode mode);

    // 編集モード変更
    void SetDirectMode();
    void SetComposingMode();
    void SetOkuriMode(char okuri);

    // 編集操作
    void Insert(char ch);
    void BackSpace();
    void Delete();
    void CursorLeft();
    void CursorRight();
    void CursorUp();
    void CursorDown();
    //...
};

今のところ「見出し語」の存在は隠蔽されていて、getter/setter は公開していません。ここで、「見出し語の補完機能」を提供する方法を考えてみます。

おそらく、一番オーソドックスなのは SKKEditor に SKKCompleter といったような補完オブジェクトを持たせることでしょう。そして、FindCompletions() や NextCompletion() 等の補完用インタフェースを追加し、その内部で SKKCompleter にメッセージを転送してあげるわけです。

// 見出し語の補完
bool SKKEditor::FindCompletions() {
    if(completer_.Execute(getEntry())) {
        setEntry(completer_.CurrentEntry());
    }

    return !completer_.IsEmpty();
}

// 次の補完
void SKKEditor::NextCompletion() {
    if(completer_.IsEmpty()) return;

    completer_.Next();
    setEntry(completer_.CurrentEntry());
}

//...

見出し語の補完機能を SKKEditor の主要な責務と見なすならこれで問題はありません。でも、このあたりの感覚は微妙なところで、個人的には SKKEditor のインタフェースが汚れるようで、どうしたものかと悩んでいました。

もう一つは SKKCompleter を表に出し、SKKEditor を接続する方法です。

// ctor injection
SKKCompleter::SKKCompleter(SKKEditor* editor) : editor_(editor) {}

// 見出し語の補完
bool SKKCompleter::Execute() {
    if(complete(editor_->GetEntry()) {
       editor_->SetEntry(currentEntry());
    }

    return !completions_.empty();
}

//...

こちらは大分良いのですが、見出し語の setter/getter が SKKEditor の公開メソッドになってしまっているのが難点です。また、SKKCompleter はもはや SKKEditor 抜きにテストができません。がちょーん。

そこで閃いたのが Buddy イディオムです。SKKCompleter と SKKEditor を分離しつつ、協調させることができます。まず、SKKCompleter を見てみます。

class SKKCompleterBuddy {
    friend class SKKCompleter;

    // 見出し語の取得
    virtual const std::string& SKKCompleterQueryString() const = 0;

    // 現在の見出し語の通知
    virtual void SKKCompleterUpdate(const std::string& entry) = 0;

public:
    virtual ~SKKCompleterBuddy() {}
};

class SKKCompleter {
    SKKCompleterBuddy* buddy_;
    // ... その他のメンバー変数

public:
    SKKCompleter(SKKCompleterBuddy* buddy) : buddy_(buddy) {}

    bool Execute() {
        // buddy_->SKKCompleterQueryString() を呼び出し、見出し語を補完する
    }

    void Next() {
        // カーソルを進めて現在の見出し語を引数に buddy_->SKKCompleterUpdate() を呼び出す
    }

    void Prev() {
        // カーソルを戻して現在の見出し語を引数に buddy_->SKKCompleterUpdate() を呼び出す
    }
};

SKKCompleterBuddy が導入されました。なんのことはない、Observer パターンの変形ですが、派生クラスがオーバーライドするメソッドは非公開になっています。これらのメソッドは friend 宣言された SKKCompleter しか触れません。当然のことながら SKKEditor は SKKCompleterBuddy を継承します。

class SKKEditor : public SKKCompleterBuddy {
    //...

    virtual const std::string& SKKCompleterQueryString() const {
        return getEntry();
    }

    virtual void SKKCompleterUpdate(const std::string& entry) {
        setEntry(entry);
    }

public:
    //...
};

これで SKKEditor のインタフェースを最小に保ちながら、見出し語の補完機能も提供することができました。二つのクラスは Buddy であり、お互いを必要としています。でも、適度な距離感が欲しいのです。そして、それぞれの個性を際立たせたい。安全に、簡単に。

継承と friend を組み合わせただけでこれほど効果的なものができるということに気付いたのは、朝の通勤電車の中でした。以前は、『friend なんて邪悪だし使う必要もない』なんて思い込んでいただけに、目から鱗ではあるんですが、嬉しいような後ろめたいような複雑な気分になりました。

なんにせよ、これでまた一歩前進できそうです。