技術者のためのC++ワンポイント
書誌
| tag | C++ |
| text | 唯野 |
| author | 真紀俊男 |
| publisher | 『Interface 2001.10-2002.8』CQ出版社 |
履歴
| 2002.6.2x ? | 読了 |
| 2002.10.17 | 公開 |
| 2004.11.1 | 修正 |
| 2012.2.7 | 修正 |
感想
上記連載の中からデザインパターンの扱った部分をまとめている。一般にデザインパターンというと例題として Java の用いられることが多く、個人的には C++ での簡便な例が欲しかったので、この記事はありがたかった。パターンという以上、いつでもその概要を参照できる状態になければ意味はないわけで、GoF 本よりも手軽に参照できる点で特に優れている。(もちろん GoF の日本語改訂版では C++ でのサンプルも独自に収録されており、これはこれでありがたい配慮である。)つまり、パターンを問題に当てはめるのではなく、問題をパターンにあてはめるための使い方ということである。
むろん、他所でもいわれているように、デザインパターンそのものは単なる雛形に過ぎないから、実際の自分の仕事への応用は個別に行わなければならない。最近では、そのような特定ドメイン向けのパターン集も存在しているので、より自分の問題に合わせたパターンの理解と実践が必要だろう。
なお、今更、雑誌を買い集めるわけにもいかない方には『TRY! PC 2002 Autumn』がこの連載記事をまとめた単行本となっているので、こちらがおすすめである。
# なお、なお手抜きのためクラス図が ASCII ART になっている
# 折を見て UML モデリングツールあたりで修正したい
# 一応、----- が抽象クラス、===== が具象クラスである
抄録
Smalltalk では Object が Observer パターンの機能を始めから持っているのでそのまま利用できる。Java では Swing が、その変形パターンを採用している。
デザインパターンに関しては GoF 本以外に POSA 本(F・ブッシュマンほか/金澤典子ほか訳『ソフトウェアアーキテクチャ ソフトウェア開発のためのパターン体系』近代科学社)という定本がある。
また、デザインパターンが主にメカニズムの設計で用いられるのに対して、他に以下のようなパターンがある。『アンチパターン』 も変種といえるだろう。
- アナリシスパターン オブジェクト指向分析でのパターン。同名の本がある。より概念レベルでのパターンを扱う。
- アーキテクチャパターン アーキテクチャ設計の中での全体的な構成におけるパターン。レイヤ構成や MVC、MFC での Dock-View など。
- パイプ & フィルタ 入力データを加工して出力するような場合に、プログラムの側をフィルタとして捉えるパターン。フィルタ同士はインタフェースをパイプとすることで独立性を保つ。しかし、データ構造が一貫しないと組み合わせに依存性を生じる場合がある。
第 13 回 グローバル変数の問題とパターン (2001.10) p.149-155
うまくいかないプロジェクトは大規模プロジェクト(ひとりでプログラムの全貌を把握できないレベル)で以下のような共通の症状がある。goto の多寡よりも複雑なプログラムになっているときが危ない。入れ子の多さなどは分割統治を使えるが、グローバル変数の多さは対処が非常に難しい。なぜなら、どこで参照・書き換えの起こるかが把握しにくく、利用側は書き換えを前提としており、うかつに修正できないことが多いため。グローバルのフラグ変数を導入して回避しようとしても、かえって問題をこじらせてしまう場合がある。
- ひとつの手続き/関数が異常に長い
- if、while、switch の入れ子レベルが異常に深い
- グローバル変数が異常に多い
これは C++ でも同様で、例えばグローバルオブジェクトを不用意に用いると初期化タイミングや複数のオブジェクト間の依存関係、インスタンスの数を制限したい場合などにまつわる問題が起こり、クラス内でグローバル変数にアクセスすることでクラスの独立性を低めたりといった問題を招きやすい。
Singleton パターンではコンストラクタ・デストラクタを非 public とし、インスタンス取得専用の static メソッドを用意することで、インスタンスの数をひとつに固定化している。更にスレッドセーフとする場合にはインスタンスへのアクセス部分をクリティカルセクションにする。つまり、クラス自身にオブジェクトの管理と生成を行わせる。
// Singleton
static uniqueInstance;
static instance()
{
if(uniqueInstance == NULL)
{
uniquInstance = new Singleton;
}
return uniqueInstance;
}
パターンとは繰り返しの後に抽出されるもので、アルゴリズムが計算問題を解決するのに対してアーキテクチャ要素を記述する(POSA 本)といわれる。一方、イディオム(定石、例えば C での二重インクルード防止や C++ での RAII : コンストラクタで資源の確保、デストラクタで資源の解放を行うテクニック、auto_ptr など)とパターンは、ある言語でのイディオムが他の言語に適用できない場合があるため、これも完全には重ならない。POSA 本ではパターンをこれら 3 つのカテゴリとして分類している。
# POSA 本とは F・ブッシュマンほか/金澤典子ほか訳『ソフトウェアアーキテクチャ ソフトウェア開発のためのパターン体系』近代科学社 のことで、原著の頭文字(Pattern Oriented Software Architecture)を取って、こう呼ばれている。GoF 本に関しては既にどこかで触れているのでここでは割愛する。
- アーキテクチャ ソフトウェアの基礎的な構造組織化スキーマの表現
- デザインパターン ソフトウェアのサブシステム、コンポーネント間の関係を洗練させるスキーマの提供
- イディオム あるプログラミング言語に特化した抽象度の低いパターンで、コンポーネント間の実装関係を表現する
その上で、POSA 本ではアーキテクチャパターンを以下のようにカテゴリ化している。
- 混沌から構造へ (From Mud to Structure)
- Layers 階層ごとの交換可能性の構築、OSI 7 層モデルなど
- Pipe and Filters データをストリームとして扱ってフィルタをかけることでオブジェクトの肥大化を防ぐとともにフィルタの交換による柔軟性を得る
- Blackboard 決定論的戦略が不明な場合の複数サブシステムが黒板コンポーネントを中心の共同作業で問題解決を試みる
- 分散システム (Distributed System)
- Broker 分散システム構築のパターン、依存関係のない複数コンポーネントが仲介者(Broker)を介して共同作業を行う
- Pipe and Filters 上述
- Microkernel システムの最低限の機能を拡張部分やクライアント依存部分と切り離すことで、拡張機能同士の協調機能を提供
- 対話型システム (Interactive System)
- Model-View-Controller 対話アプリケーションを MVC として分割することで相互の影響を抑える
- Presentation-Abstraction-Control MVC に似ているが、それを PAC として分割
- 適合型システム (Adaptable Sysem)
- Reflection システムの構造とふるまいを動的に変更するメカニズムの提供
- Microkernel 上述
第 14 回 分割とパターン (2001.11) p.179-187
分割とは「分解」ではなく「合成」を目指すものである。また、規模以上に内容が重要である。特に安易に分割したものをグローバル変数で連絡させようとすると破綻しやすい。そして、パターン分割には密接な関係がある。
Facade パターン(ファザード : 建物の正面)は複数のユニットをひとつのように見せかけるもので、窓口となるクラスを提供することで内部での複雑さを軽減する。こうすることでクラス間の関係をすっきりさせることができる。よくあるのは複数機能の組み合わせを提供するクラスではなく、既存のクラスに新たな機能を追加してしまう場合で、Facade はあくまでもこのようなコピー・改造とは別物となる。
Facade に近いものとして Mediator パターンがある。これは Mediator (仲介者)をひとつ決めておいて、情報の流れをそこに集中化させることで混乱を防ぐというもの。
Mediator (抽象クラス) Colleague (基底クラス)
-------- =========
↑ 継承 ↑↑↑ 継承
ConcreteMediator (具象クラス) ConcreteColleague1,2,3 (具象クラス)
================ ======================
↑ ↑
└---------------┘
連絡
第 15 回 Strategy パターンと Bridge パターン (2002.1) p.170-177
従来のプログラムが実装そのものであったのに比べると、オブジェクト指向においてはインタフェースと実装の分離が強調される。例えば C++ なら継承を用いてインタフェースを基底の抽象クラスとし実装を派生クラスで行うなど。しかし、実装の分離は必ずしも継承を伴うわけではなく、メンバ変数であるクラスオブジェクトに処理を横流しすることでも行える。これを一般に委譲、デレゲート(delegate)などと呼ぶ。これらは単純に処理をパッチ/フックしているだけだが、自分では処理を行わないことでクラスの肥大化を防いでいる。(委譲には継承階層が低く抑えられる、継承によるメンバ関数の増加が起きにくい、委譲元からは必要なメッセージしか届かない――などの利点がある。)
デザインパターンには「実装の交換」を扱うものがいくつかあるが、Strategy パターンは適用できるアルゴリズムを交換可能にすることで(アルゴリズムをオブジェクトとみなすことで)、状況に応じた切替の行うメカニズムを指す。つまり、この場合、インタフェースとなる基底クラスはひとつで、個々のアルゴリズムが複数の派生クラスとなる。
委譲
Context ------------→Strategy
======= --------
委譲先への参照を持つ ↑ ↑
↑ | ConcreteStrategy2
|サービスの利用 | =================
| ConcreteStrategy1
他のオブジェクト =================
================
Strategy パターンに似たものとして State パターンがあるが、こちらは(Strategy パターンでは同じものを使い続けることがありえるが)もっと内部の状態に応じて積極的に派生クラスを切り替えていく点が異なる。例えば、状態遷移を共通のインタフェースを持った個々の状態に対する多態として実現するなど。
同様に OS への依存を避けるクラスライブラリなどが、インタフェースはひとつで内部に OS ごとの実装を持つような場合がある。ただ、そのような場合、ライブラリの扱うガジェット(gadget : 仕掛け、仕組み)は複数必要になることが多いため(Window だけでなく Icon など)、インタフェース側と実装側での派生関係も複雑になりやすい。Bridge パターンはインタフェースと実装を分離しつつ、更に継承ツリーも分離することで拡張性を確保したい場合に使う。
例えば、表示を行う実装 : m と 表示方法 : n の組み合わせというとき、数が少なければ個別の Strategy パターンでも対処できるが、組み合わせが m * n というときの数を抑えるとき、Bridge パターンが用いられる。
委譲
Abstraction---------------------→Implementer
=========== -----------
委譲先への参照を持つ ↑ ↑
↑ ↑ 継承 | ConcreteImplementer2
| RefinedAbstraction | ====================
| ================== ConcreteImplementer1
他のオブジェクト ↑ ====================
================ 他のオブジェクト
サービス利用 ================
サービス利用
第 16 回 Iterator パターンと Adapter パターン (2002.2) p.158-164
STL でのイテレータと同じように、複数のデータ内を順に走査していくオブジェクトを Iterator という。Iterator を用いると、利用側がデータの構造を意識せずに済み、データの実装が変更されても影響を受けにくくすることができる。但し、実際の実装では Iterator が機能を持ち過ぎても意味のないことが多いので、提供するサービスは絞るのがよい。また、ここでも抽象クラス側でインタフェースを提供することにより実装とを分離している。そうすることでインタフェースから何のサービスが提供されているか、異なる実装による共通部分として何があるか――の把握を容易にできる。なお、Iterator パターンに似た複数データを走査していくパターンとして Visitor パターンがある。
Iterator
--------
発生 ↑
ConcreteAggregate----→ConcreteIterator
=================←――================
集約オブジェクト 走査
STL でのアダプタと同じように、既に存在するクラスやオブジェクトを元に、新たなサービス(インタフェース)のクラスやオブジェクトを作るのが Adapter パターンとなる。一からオブジェクトを作らずに似たものを流用するということで、作成方法にはふたつのアプローチがある。即ち、クラスアダプタなら差分プログラミング、オブジェクトアダプタなら利用サービスの横流しを行う。
- クラスアダプタ 流用される側(Adaptee)を継承する
- オブジェクトアダプタ Adaptee を包含する
[クラスアダプタ] [オブジェクトアダプタ]
Adaptee
=======
↑継承
利用 | 利用 委譲
利用者----→Adapter 利用者----→Adapter----→Adaptee
====== ======= ====== ======= =======
クラスアダプタとオブジェクトアダプタのいずれを使うかに関しては、以下のような基準を元にすればよい。ただ C++ のような継承の楽な言語では、基本的にクラスアダプタの方がコーディング量は少なくなる。また、アダプタは、あるクラスインタフェースの利用や変更が難しくインタフェースの変換(ラッパー : Wrapper)を提供するような場合でも利用される。
- 使い回すクラスと利用するクラスの関係が is-a なら継承(クラスアダプタ)
- 使い回すクラスと利用するクラスの関係が has-a なら所有(オブジェクトアダプタ)
アダプタに似て、使いにくいオブジェクトを使いやすくするためのパターンとして Proxy パターン(別名サロゲート : Surrogate)がある。例えば、代理オブジェクトが本物のオブジェクトを必要とされた時点で作成しそちらへ委譲したり(virtual proxy)、本物のオブジェクトがネットワーク越しか別プロセスにあるため、代理オブジェクトに本物のオブジェクトとの通信を任せて本物のオブジェクトのように振舞わせる(remote proxy)など。virtual proxy の例としてディスクキャッシュ、remote proxy の例として CORBA や DCOM がある。
第 17 回 Decorator パターン, Composite パターンと Chain Of Responsibility パターン (2002.3) p.158-164
継承は機能の追加に向いているように見えるが、複数回の機能加算や異種の加算は行いにくいという特徴がある。そこで委譲を使うが、委譲先オブジェクト X と Y の関係で X の結果を Y に渡すという直列性に着目するのが Decorator パターンの発想になる。例えば、あるボタンに飾り付けを行うとき、飾り付けを担当する専用オブジェクト(Decorator)によって機能追加を個々のオブジェクトに対し動的に行うのが Decorator パターンとなる。
Component (飾り付けと飾り付けされるオブジェクトの基底クラス) --------- ↑ ↑ (ここが継承関係にあるので、派生クラス全体を委譲先にできる) | Decorator (飾り付けするオブジェクトへの参照を持つ抽象クラス) | --------- | ↑ ↑ | | ConcreteDirectorB (飾り付け手段B) | | ================= | ConcreteDirectorA (飾り付け手段A) | ================= ConcreteComponent (飾り付けされる具象クラス) =================
Composite パターンとは基本要素と合成物に同じインタフェースを提供することでプログラムの負担を減らすパターンをいう。例えば図形などで基本要素だけでなく合成したもの(Composite)を利用した場合、インタフェースが異なると、それだけプログラムは複雑になりやすい。そこで共通の基底クラスを用意して利用者が統一されたインタフェースを利用できるようにする。合成物の構成要素に対する処理が再帰できるようなときに利用されやすい。
Client----→Component (基本要素と合成物の共通基底クラス)
====== ---------
利用者 ↑ ↑
| Composite (合成物の具象クラス)
| =========
Leaf (基本要素の具象クラス)
====
Composite パターンでは実際の利用ではボスとなるオブジェクトの取得などが必要になる。また、連結先が非常に多くなる場合、オブジェクトをうまく連結しておかないとパフォーマンスが低下したり処理が複雑化する。そのため処理対象の検索方法と実際の処理を行うオブジェクトを明確にしておく。(GUI プログラムなら処理対象自身がイベントハンドラを持つのが一般的。こういうものが一箇所に集まると複雑化したときに手がつけられなくなる。)
Composite パターンは木構造における全体(節)-部分(葉)に同じインタフェースを適用するような場合でも利用される。例えば、VCL の TComponent ではこのパターンを変形して使用しており、これによって全てのコンポーネントが木構造を持ったものとして管理される。その結果、親オブジェクトが削除されると、関連する全てのオブジェクトが削除されるようになっている。これは Decolator パターンの異なる目的での利用である。
ある処理で、要求に合致するオブジェクトを順に探して見つかるまでチェーン状につながったオブジェクトを伝播させていくのが Chain Of Responsibility パターンとなる。ポイントは successor と呼ばれる自分が処理オブジェクトでないときに横流しするオブジェクトへの参照で、これを基底クラスで持っておいて、実際の横流しは ConcreteHandler が行う。ConcreteHandler は同士は異なるクラスでも構わない。
Client----→Handler (successor を持つ基底クラス)
====== -------
利用者 ↑ ↑
| ConcreteHandler2 (処理オブジェクト2)
| ================
| / ←横流しするときに
| / successorをたどる
ConcreteHandler1 (処理オブジェクト1)
================
これを使うと処理の集中化を避けることができるが、逆に誰もが責任を負わなかったり、どこで処理されたのかが分からなくもなりやすい。処理オブジェクトが見るからなかった場合なども考慮しなければならない。
上記を組み合わせると、例えば Composite パターンを使って Chain Of Responsibility の段数を減らし負荷分散させるなどが可能になる。
全文を読まれる場合はログインしてください
