デザインパターンって?
デザインパターンはJava/C++に向けて考案された、クラス設計及び実装テクニックのまとめです。ところが、いまどきのC++には少し当てはまらないことがあったり、実際には複雑でることが常なので多くはGoFのデザインパターンに従っていますが、ここでは都合により追加/削除させて頂きました。
クラス設計も多少含まれますが、クラス設計なんてものはアプリケーションによって全く異なるので敢えて強制しようと思いません。ただし、ここで紹介しているパターンはその構成要素の1つとなり得るでしょうから、どれも理解しているべきです。
何が嬉しいか、何が嬉しくないのかはこれらのパターンを採用する際に判断することなので余り言及しません。
クラス構成の一部
ほとんどがインターフェイスを使いましょうといった基本的なことで構成されています。
Template Methodパターン
(1) ある共通の手続きがあって (2) 手続きの中の一部分だけが違うクラスがたくさんあったとき (3) 抽象メソッドにして派生クラスで実装するパターンです。これは抽象メソッド=仮想関数の一般的な使い方なのでパターンと言うほどのものではありません。
class A {
void f() {
methodA(); // 派生先で実装
methodB(); // 派生先で実装
}
virtual void methodA() = 0;
virtual void methodB() = 0;
};
class C : public A {
void methodA() {
// ここに処理
}
void methodB() {
// ここに処理
}
}
Builderパターン
(1) 生成方法がパターン化されていて (2) それらの方法を組み合わせる方法が複数あるとき (3) 生成方法を抽象化して (4) 組み合わせる方法を、(3)で抽象化したもので表現するパターンです。
以下の例では、B1, B2で生成方法をmethodA, mehthodBの2通りに抽象化しました。B1, B2は抽象化したIで同一視できます。続いて、生成方法はB1, B2に関わらず、Iのみで表現できます。
結局、似たようなものを抽象化してインターフェイスにしたものなのでごく当たり前の発想といえます。
#include <iostream>
using namespace std;
class I {
public:
virtual void methodA() = 0;
virtual void methodB() = 0;
};
class B1 : public I {
public:
void methodA() { cout << "In B1::methodA()" << endl; }
void methodB() { cout << "In B1::methodB()" << endl; }
};
class B2 : public I {
public:
void methodA() { }
void methodB() { cout << "In B2::methodB()" << endl; }
};
void func1(I& a) {
a.methodA();
a.methodB();
}
void func2(I& a) {
a.methodA();
a.methodB();
a.methodA();
}
int main(void) {
B1 b1;
B2 b2;
cout << "生成方法func1, 生成者B1" << endl;
func1(b1);
cout << "生成方法func1, 生成者B2" << endl;
func1(b2);
cout << "生成方法func2, 生成者B1" << endl;
func2(b1);
cout << "生成方法func2, 生成者B2" << endl;
func2(b2);
cin.get();
return 0;
}
出力は、
生成方法func1, 生成者B1
In B1::methodA()
In B1::methodB()
生成方法func1, 生成者B2
In B2::methodB()
生成方法func2, 生成者B1
In B1::methodA()
In B1::methodB()
In B1::methodA()
生成方法func2, 生成者B2
In B2::methodB()
Facadeパターン
複雑なクラス群を全てまとめて、1つのクラスでインターフェイスを提供するというもの。複雑なクラス群があったら誰でもそういう設計を仕様と思うのでごく当たり前のクラス設計といえます。
もちろん、C++ではFはA, B, Cを包含しないでprivate継承するという方法もあります。其の方が自然な場合もありますが、包含に比べて拡張性に乏しいのが大きなデメリットですね。
#include <iostream>
using namespace std;
class A {
public:
void func() {
cout << "In A::func()" << endl;
}
};
class B {
public:
void func() {
cout << "In B::func()" << endl;
}
};
class C {
public:
void func() {
cout << "In C::func()" << endl;
}
};
class F {
private:
A a;
B b;
C c;
public:
void test() {
a.func();
b.func();
c.func();
}
};
int main(void) {
F f;
f.test(); // fを使えばA, B, Cを知らなくても全て解決!
cin.get();
return 0;
}
Factory Methodパターン
(1) 共通なインターフェイスを持ったオブジェクトO1, O2があって、(2) それらを生成するようなクラスF1, F2があったとき、(1)(2)をそれぞれインターフェイスを以て共通化しましょうね、というお話です。
C++ではいらないところにnewを使うべきではありません。従って以下の例は最悪です。newしたのにdeleteさせるというのはあってはいけません。せめて、deleteさせないようにdestoryメソッドなど用意して隠蔽すべきです。スマートポインタなど検討するのも良いでしょう。
#include <iostream>
using namespace std;
// Factoryで生成されるインスタンスの共通I/F
class I {
public:
virtual void func() = 0;
};
/* Factoryで生成される具体的なもの */
class O1 : public I {
public:
void func() {
cout << "In O1::func()" << endl;
}
};
class O2 : public I {
public:
void func() {
cout << "In O2::func()" << endl;
}
};
// FactoryのI/F
class FI {
public:
virtual I* create() = 0; // 生成インターフェイス
};
class F1 : public FI {
public:
I* create() {
return new O1();
}
};
class F2 : public FI {
public:
I* create() {
return new O2();
}
};
int main(void) {
F1 f1;
F2 f2;
I* o1 = f1.create();
I* o2 = f2.create();
o1->func();
o2->func();
delete o1;
delete o2;
cin.get();
return 0;
}
Adapterパターン
クラスのラッパーです。あるクラスを包み込んで、直接そのクラスを知らなくても操作できるようにするパターンです。これに関しては、クラスのページで議論します。
Proxyパターン
同じインターフェイスを持ったクラスで、元のクラスを移譲して元のクラスの代わりになるというものです。ラッパーの特殊なものといえます。ポイントは、元のクラスCもそれをラップするPも共通のインターフェイスを持っている点にあります。
#include <iostream>
using namespace std;
class I {
public:
virtual void func() = 0;
};
class C : public I { // CがIを継承していることに注目
public:
void func() {
cout << "In C::func()" << endl;
}
};
class P : public I { // こちらもIを継承していることに注目
private:
C* c;
public:
P() : c(new C()) { }
~P() { delete c; }
void func() {
c->func();
}
};
int main(void) {
P p;
p.func();
cin.get();
return 0;
}
継承階層を移譲によって分離
移譲を積極的に使ってクラス構造を見直すアプローチをしたデザインパターンです。そもそも、機能の拡張を継承でしてしまうような設計が悪いのでごく当たり前の対応といえます。
ただ、Compositeパターンは有用と思います。
Observerパターン
(1) あるクラスがあって (2) そのクラスに処理を頼んで (3) 頼まれたクラスに結果を報告する必要があるときに使うパターンです。要するにコールバックです。
以下では、Cが共通のイベント通知I/Fを持っているクラスを持っていて、Cの処理(executeのなか)が終わったときに保持しているクラスにupdate関数を使って通知するというものです。
#include <iostream>
using namespace std;
class I {
public:
virtual void update() = 0;
};
class A : public I {
public:
void update() {
cout << "In A::update()" << endl;
}
};
class B : public I {
public:
void update() {
cout << "In B::update()" << endl;
}
};
class C {
private:
I* i;
public:
void set(I* i) {
this->i = i;
}
void execute() {
i->update();
}
};
int main(void) {
A a;
B b;
C c;
c.set(&a);
c.execute();
c.set(&b);
c.execute();
cin.get();
return 0;
}
Bridgeパターン
(1) あるクラスCがあって性質A, Bを加えたCA, CBがあって、それぞれ性質a, bを加えたCAa, CAb, CBa, CBbというクラスがあったとき、 (2) 性質A, Bを性質I, 性質a, bを性質iとし、CはIとiを保持するというクラス構成に替えるパターンです。
もちろん、CA, CBは保ったまま、Cはiを保持するという継承 & 包含にしてもいいですし、CはIとiをprivate継承する多重private継承にするという手法もあるとおもいます。(やったことないですがw)
#include <iostream>
using namespace std;
class I {
public:
virtual void func() = 0;
};
class A : public I {
public:
void func() {
cout << "In A::func()" << endl;
}
};
class B : public I {
public:
void func() {
cout << "In B::func()" << endl;
}
};
class i {
public:
virtual void func() = 0;
};
class a : public i {
public:
void func() {
cout << "In a::func()" << endl;
}
};
class b : public i {
public:
void func() {
cout << "In b::func()" << endl;
}
};
class C {
private:
I* m_I;
i* m_i;
public:
C(I* _I, i* _i) : m_I(_I), m_i(_i) { }
void func() {
m_I->func();
m_i->func();
}
};
int main(void) {
A i_A;
b i_b;
C c(&i_A, &i_b);
c.func();
cin.get();
return 0;
}
Compositeパターン
(1) あるクラスがあって (2) そのクラスに包含されるべきクラスがあって (3) それら2つのクラスを同一視(=共通のインターフェイスを持つ)することで (4) 再帰的な構造を表現できるというパターンです。
以下の例では、CがあってLはCに包含されるべきですが、CとLに共通なインターフェイスCBを持たせて、CはCBを包含するようにすれば、Cは中にCあるいはLを含めることができるということを表現しています。
#include <iostream>
using namespace std;
class CB {
public:
virtual void func() = 0;
};
class C : public CB {
private:
CB* cb;
public:
C(CB* cb) : cb(cb) { }
C() : cb(nullptr) { }
void func() {
cout << "In C::func()" << endl;
cb->func(); // 階層の再帰
}
};
class L : public CB { // LもCもCBを継承しているのが肝
public:
void func() {
cout << "In L::func()" << endl;
}
};
int main(void) {
L l;
C c(&l); // コンストラクタで階層構造を再帰的に規定
C c2(&c); // コンストラクタで階層構造を再帰的に規定
c2.func();
cin.get();
return 0;
}
なお、静的な階層関係を規定するだけならテンプレートを使ったパラメータ化継承を検討してみて下さい。
Decoratorパターン
Compositeパターンとクラス構造は同じです。(1) ある共通なインターフェイスを持ったクラス群があって (2) それらに機能を追加しようとする複数のクラスがあったとき、それらのクラスにも同じインターフェイスをもって、そのインスタンスを包含させれば再帰的に機能を追加できるよね、というお話です。
#include <iostream>
using namespace std;
class I {
public:
virtual void func() = 0;
};
class A : public I {
public:
void func() {
cout << "In A::func()" << endl;
}
};
class B : public I {
public:
void func() {
cout << "In B::func()" << endl;
}
};
class C : public I { // Point1 ここでIを実装する
private:
I& i; // Point2 ここで包含する
public:
C(I& i) : i(i) { }
C(C& it) : i(it) { } // これがないとクラスCを包含できない
void func() {
cout << "拡張";
i.func();
}
};
int main(void) {
A a;
B b;
C c1(a);
C c2(b);
C c3(c2);
cout << "c1:" << endl;
c1.func();
cout << "c2:" << endl;
c2.func();
cout << "c3:" << endl;
c3.func();
cin.get();
return 0;
}
出力は、
c1:
拡張In A::func()
c2:
拡張In B::func()
c3:
拡張拡張In B::func()
C++では、自分のクラスの参照はコピーコンストラクタという特別なコンストラクタに分類されるため、
C(C& it) : i(it) { }
を定義しないと、デフォルトのコピーコンストラクタが呼び出され、
C c3(c2);
を実行すると、c3はc2のコピーと見なされてしまいます。参照ではなくポインタを使えば関係ありません。参照を使うかポインタを使うかはここでは議論しないこととします。
Strategyパターン
Bridgeと同じ継承構造を持つ。コンストラクタでクラスを選択して、そのクラスに移譲してクラスを構築する方法です。C++ではテンプレートによって静的に表現できるため、以下の方法は無駄があるといえます。
#include <iostream>
using namespace std;
class I {
public:
virtual void func() = 0;
};
class S1 : public I {
public:
void func() {
cout << "In S1::func()" << endl;
}
};
class S2 : public I {
public:
void func() {
cout << "In S2::func()" << endl;
}
};
class C {
private:
I* i;
public:
C(I* i) : i(i) { }
void func() { i->func(); }
};
int main(void) {
S1 s1;
S2 s2;
C c1(&s1);
C c2(&s2);
c1.func();
c2.func();
cin.get();
return 0;
}
C++ではテンプレートがあるので、インスタンスを生成する必要がない。ポインタを使うのもかっこわるい。
これはポリシークラスと呼ばれています。
#include <iostream>
using namespace std;
class S1 {
public:
void func() {
cout << "In S1::func()" << endl;
}
};
class S2 {
public:
void func() {
cout << "In S2::func()" << endl;
}
};
template<class S>
class C : private S {
public:
void func() { S::func(); }
};
int main(void) {
C<S1> c1;
C<S2> c2;
c1.func();
c2.func();
cin.get();
return 0;
}
Stateパターン
Strategyと同じクラス構成です。但し、こちらの場合はインターフェイスが常に入れ替わる可能性があります。
#include <iostream>
using namespace std;
class S {
public:
virtual S* func() = 0;
};
class S1 : public S {
public:
S* func();
};
class S2 : public S {
public:
S* func();
};
S* S1::func() {
cout << "In S1::func()" << endl;
return new S2();
}
S* S2::func() {
cout << "In S2::func()" << endl;
return new S1();
}
class C {
private:
S* s;
public:
C() : s(new S1()) { }
~C() { delete s; }
void func() {
S* temp;
temp = s->func();
delete s;
s = temp;
}
};
int main(void) {
C c;
c.func();
c.func();
cin.get();
return 0;
}
Commandパターン
Stateが状態でなくコマンド要求になっただけですので省略。
機能の分散
一つのクラスがたくさんの仕事を持ってしまうと、そのクラスを使う人は大変です。クラスの機能を分散する目的のパターンはこちらです。
Iteratorパターン
(1) ある多数のインスタンス/値をまとめたコレクションクラスがあって (2) それらの全て或いは特定の範囲にアクセスする必要がでたとき (3) 外部にその処理を追いやりつつ、内部の詳細は公開したくないときに使うパターンです。C++では当たり前のイテレータです。
イテレータについてはSTLで触れますのでここでは省略します。なぜならば、STLで使われているイテレータを標準として使うべきであり、俺俺イテレータを再発明する必要はないからです。
Visitorパターン
オーバーロードを使ってクラスごとに処理を振り分ける手法です。ダブルディスパッチと言われています。これは、実質funcが(A1, A2), (C1, C2)の4通りの中から選択して実行されていることになるからです。
#include <iostream>
using namespace std;
class C1;
class C2;
// クラスC1・C2で振り分ける処理がたくさんあると仮定します
class I {
public:
virtual void func(C1&) = 0;
virtual void func(C2&) = 0;
};
class A1 : public I {
public:
void func(C1&) {
cout << "A1::func() # C1" << endl;
}
void func(C2&) {
cout << "A1::func() # C2" << endl;
}
};
class A2 : public I {
public:
void func(C1&) {
cout << "A2::func()" << endl;
}
void func(C2&) {
cout << "A2::func() # C2" << endl;
}
};
// 振り分けられる対象
class C {
public:
virtual void func(I& i) = 0;
};
class C1 : public C {
public:
void func(I& i) {
i.func(*this); // 自身で判断させる
}
};
class C2 : public C {
public:
void func(I& i) {
i.func(*this); // 自身で判断させる
}
};
int main(void) {
// 振り分けられる対象
C1 c1;
C2 c2;
// 振り分ける対象
A1 a1;
A2 a2;
// 振り分けられる側が自分自身で判断して勝手に振り分けられる
c1.func(a1);
c2.func(a1);
cin.get();
return 0;
}
最終更新:2014年01月15日 03:23