パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
デザインパターン

デザインパターンはソフトウェア開発において過去の設計ノウハウを蓄積し命名してカタログ化したもの。

その他の外部情報

本サイトでの解釈

デザインパターンという用語はエーリヒ・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディースの著した書籍(Erich Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software, Boston, Addison-Wesley, 1995; Boston, Addison-Wesley, 2002)が初めて導入した。4名はGoF(Gang of Four)と呼ばれ、また書籍自体もGoFと参照される事が多い。以下においてGoFは書籍を指すものとする。GoFはデザインパターンのバイブル的地位にあり本サイトも準拠するが、プログラミングのメタモデルはかなりの難物と言わざるを得ない。

覚え書き
GoFのサンプルコードは幸いC++で書かれているが、ドキュメントエディタを具体例とするそれはあまり理解を助けず、恐らくサイト作成者に対象ドメインの十分な理解が欠けているためであろう。パターン名は一般的な単語が選ばれているため多くがC++の別文脈でも使用される事に注意しよう。例えばテンプレートメソッド(Template Method)はC++テンプレートと何の関係も無い。一方イテレータ(Iterator)は標準テンプレートライブラリ(STL)のイテレータにほぼ対応する。コンポジット(Composite)はツリー構造を指しUMLクラス図のコンポジション(Composition)と完全に意味が異なる。
覚え書き
GoFの列挙するデザインパターンに一貫性を感じない。インタープリタ(Interpreter)やメメント(Memento)はオブザーバー(Observer)やストラテジー(Strategy)と同列に扱うほどに一般的だろうか。ファサード(Facade)とメディエイター(Mediator)は同じものとしか思えないし、アダプター(Adapter)とプロクシ(Proxy)だって別物とするほどに違うのか。これらはサイト作成者の理解不足としても、デザインパターンとはプログラミングの経験に依存する主観的要素が大きい概念なのだろう。

最初に本サイトの解釈するデザインパターンの意義を説明してから、本サイトが参照するいくつかのデザインパターンについてウィキペディア(日本語)説明と共に考察する。

デザインパターンの意義

本サイトのプログラミングは統合開発環境ウィザードの生成するスケルトンからスタートする。仕様書も設計書も存在せず頭の中のイメージを頼りにひたすらコーディングする。成果物はインクルード/ソースコードファイルのみ、気が向けばDoxygen形式コメントで説明を追加する。他人は想定せずとも半年後に機能拡張またはバグ取りを迫られる自分自身への説明をしておくに越したことはない。デザインパターンはプログラミング後にそういった説明をするためのものであって、けしてプログラミング前にアプリケーションを設計するものではない。ソースコードをデザインパターンに照らして若干のリファクタリングを施し適切なコメントを加えれば半年後の労力は大きく削減できる。教科書をサイト作成者訳で引用する(Steve McConnell, Code Complete Second Edition, Redmond, Microsoft Press, 2004, pp.104-105)。

もしあなたがデザインパターンを知らずとも「ああ、こういったアイデアのほとんどは知っている」と言うだろうが、それこそが最大の価値なのだ。各々のデザインパターンは大多数の経験豊富なプログラマにとってよく知られたアイデアだが、それに適切な名称を与える事でプログラマ間のコミュニケーションを円滑にする。... ソースコードをデザインパターンに無理やり合わせると大抵失敗する。少しの書き換えで良く知られたデザインパターンに合わせることができればソースコードの可読性は向上する。しかしデザインパターンに合わせるために大きな書き換えを必要とする場合は余計な複雑化を伴う事が多い。

ファクトリーメソッド

ファクトリーメソッド(Factory Method)は、実際に生成されるインスタンスに依存しない、インスタンスの生成方法を提供する(GoF, pp.107-116)。

ファクトリーメソッドは仮想コンストラクタ(Virtual Constructor)とも呼ばれ、C++のコンストラクタは仮想化できないメンバ関数なので意味論(セマンティクス)において実現する。GoFは対象クラスを構築するCreatorクラスを別に準備するが、自由関数あるいはスタティックメンバ関数の方が簡単でむしろ一般的だと思う。本サイトはpimplイディオムを組み合わせてファクトリーメソッドをさらに隠蔽する。

SELECTOR列挙型でロジック実装を切り替えるTMyClassクラスを例示する。ロジック実装はSELECTORを仮引数とするImplクラステンプレートの明示的特殊化で行い、明示的特殊化は全てImplBase抽象クラスの派生とする。TMyClassはImplBaseをpimpl_に保持し、インクルードファイルで定義する。

class TMyClass
{
public:
enum class SELECTOR {TYPE1,TYPE2};
private:
class ImplBase;
template<SELECTOR> class Impl;
std::unique_ptr<ImplBase> pimpl_;
public:
TMyClass(SELECTOR sel);
~TMyClass();
void DoSomething();
};

ImplBase、Implはソースコードファイルで定義してTMyClass外部から隠蔽する。

class TMyClass::ImplBase // ロジック実装が基底とする抽象クラス
{
public:
virtual ~ImplBase() {}
virtual void DoSomething()=0;
static ImplBase* Create(SELECTOR); // ファクトリーメソッド宣言
};
template<> class TMyClass::Impl<TMyClass::SELECTOR::TYPE1>:public ImplBase // テンプレート明示的特殊化によるロジック実装クラス1
{
public:
void DoSomething() override
{
... // TYPE1の処理を行う
}
};
template<> class TMyClass::Impl<TMyClass::SELECTOR::TYPE2>:public ImplBase // テンプレート明示的特殊化によるロジック実装クラス2
{
public:
void DoSomething() override
{
... // TYPE2の処理を行う
}
};
TMyClass::ImplBase* TMyClass::ImplBase::Create(SELECTOR sel) // ファクトリーメソッド定義
{
switch (sel)
{
case SELECTOR::TYPE1:return new Impl<SELECTOR::TYPE1>{};
case SELECTOR::TYPE2:return new Impl<SELECTOR::TYPE2>{};
}
return nullptr;
}
TMyClass::TMyClass(SELECTOR sel):pimpl_{ImplBase::Create(sel)} {}
TMyClass::~TMyClass() {}
void TMyClass::DoSomething() {pimpl_->DoSomething();}

シングルトン

シングルトン(Singleton)は、あるクラスについてインスタンスが単一であることを保証する(GoF, pp.127-134)。

シングルトンは改良されたグローバル変数に過ぎず忌避すべきとの意見も多いが、個人レベルの開発でそこまで言い切る必要は無いだろう。例えばサンプルコードはオプション設定ダイアログ(TMyOptionDialog)をシングルトンで実装する。さもなくばアプリケーションクラス(KTxtEditApp)のメンバ変数に所有(コンポジション)してサブオブジェクトへポインタ/参照を渡すが、機能拡張でサブオブジェクトの数と深さは増大しその度に余分な仮引数をコンストラクタに加えるのはいかにも面倒くさい。

本サイトはシングルトンにスコット・メイヤーズの方法(Scott Meyers, Effective C++ Second Edition , Reading, Addison-Wesley, 1998, pp.222-223)を採る。

ブリッジ

ブリッジ(Bridge)は、クラスなどの実装と、呼出し側の間の橋渡しをするクラスを用意し、実装を隠蔽する(GoF, pp.151-161)。

本サイトの多用するpimplイディオムはブリッジパターンの応用と見なす。いくつかの教科書(Scott Meyers, Effective C++ Second Edition , Reading, Addison-Wesley, 1998, p.165、Stephen C. Dewhurst, C++ Gotchas, Boston, Addison-Wesly, 2003, p.21)がpimplイディオムを単にブリッジパターンと参照したためサイト作成者はブリッジパターン=pimplイディオムと誤解した時期もあったが、あくまでもブリッジパターン⊃pimplイディオムである。

ただしブリッジパターン⊅pimplイディオムとの主張も多く、それに沿えば前者はオブジェクト指向設計の手法だが後者はファイルの物理的記述手法に過ぎない(リンク先より引用、原文は"So the Bridge pattern is about object-oriented design, while the PIMPL idiom is about physical design of files.")。確かにデザインパターンは設計手法の集合と捉えるのが一般的だがそれに限定する必要も無く、例えばフライウェイト(Flyweight)は設計と言うよりはオブジェクトの効率化手法であろう。pimplイディオムが物理的記述手法に過ぎないとしても、C++の可視性に関する欠陥をブリッジパターンで解決するものと位置付けるのは正当と考える。

イテレータ

イテレータ(Iterator)は、複数の要素を内包するオブジェクトのすべての要素に対して、順番にアクセスする方法を提供する(GoF, pp.257-271)。

標準テンプレートライブラリ(STL)のイテレータはGoF出版の1995年よりはるか以前に存在していた。多くのプログラミング言語が構文/ライブラリで実現済みのイテレータをデザインパターンとして一般化した概念がイテレータパターンである。標準的なC++プログラマであればGoFに到達する以前にSTLのイテレータを会得するので、GoFの定義は今更だしライブラリニュートラルなサンプルコードは役に立たない。STLの供給する数多くのイテレータは需要のほとんどを満たし、そうでない特殊なケースでも総称プログラミングにおけるコンセプトに沿うイテレータクラスを自作すればSTLのアルゴリズムはそのまま流用できる。

オブザーバー

オブザーバー(Observer)は、インスタンス(サブジェクト)の変化を他のインスタンス(オブザーバー)から監視できるようにする(GoF, pp.293-303)。

イベント駆動型プログラミングはこのパターンの主要な応用であるが、通常は出版/購読型モデルとして参照される。オブザーバーパターン⊃出版/購読型モデルであり、後者はサブジェクトとオブザーバーの間にイベントを介在させてカップリングを弱めたものとされる。前者をサブジェクトがオブザーバーのメンバ関数を直接コールする場合に限定して、オブザーバーパターン⊅出版/購読型モデルとする主張もある。サンプルコードはオプション設定ダイアログ(TMyOptionDialog)を新たなオブザーバーパターンで作成したが、wxWidgetsイベントシステム(ライブラリ供給の出版/購読型モデル)への追加として作成する事も当然可能であった。

サブジェクトとしてTMySubjectクラスを例示する。Notifyメンバ関数をコールすれば登録したオブザーバー全てのUpdateメンバ関数をコールする。実用では状態変化でサブジェクト自らがNotifyをコールする。ネストクラスにObserver抽象クラスを定義し、オブザーバークラスはこれを継承する。Observerはコンストラクタ/デストラクタ共にprotectedとして派生クラスからだけ構築/解体できる。Observerクラスポインタによる解体は抑止されてデストラクタを仮想とする必要は無い。オブザーバークラス(Observer派生クラス)はUpdateメンバ関数のみをオーバーライドし、サブジェクトへの登録/解除はObserverコンストラクタ/デストラクタが自動で行う。オブザーバーインスタンスはただ一つのTMySubjectインスタンスをサブジェクトにできる。複数のTMySubjectインスタンスをサブジェクトとするのは少し面倒で、そういった場合は出版/購読型モデルが結局手っ取り早い。一方で、多重継承すれば他のサブジェクトクラスの(ただ一つの)インスタンスを同時にサブジェクトとする事はできる。

class TMySubject
{
public:
class Observer
{
private:
TMySubject& subject_;
protected:
Observer(TMySubject& subject):subject_{subject} {subject_.Attach(this);}
~Observer() {subject_.Dettach(this);}
public:
virtual void Update(const TMySubject&)=0;
};
private:
std::set<Observer*> observers_;
void Attach(Observer* observer) {observers_.insert(observer);}
void Dettach(Observer* observer) {observers_.erase(observer);}
public:
void Notify() {for (auto* p:observers_) {p->Update(*this);}}
};

テンプレートメソッド

テンプレートメソッド(Template Method)は、あるアルゴリズムの途中経過で必要な処理を抽象メソッドに委ね、その実装を変えることで処理が変えられるようにする(GoF, pp.325-330)。

大雑把にはC++仮想メンバ関数はテンプレートメソッドと言い切れる。基底クラスの純粋仮想メンバ関数が抽象メソッドであり、派生クラスがオーバーライドでその実装を変える。基底が純粋仮想メンバ関数でない場合とか、パブリックな仮想メンバ関数一つでアルゴリズムを実装する場合とか、様々なバリエーションはGoFの定義を逸脱するように見えるとしても、これらを含めて一般化できる。逆に言えば、仮想メンバ関数は必ずGoFのテンプレートメソッドに合致するコーディングを行うべきである(Herb Sutter, Exceptional C++, Boston, Addison-Wesley, 2000; Boston, Addison-Wesley, 2002, p.84)。