はじめに

一流の開発者になりたいと願っても、 どのようにすればそうなれるのかを知っている開発者は少ないのではないかと思います。 そういったことを指導してくれるメンターにはなかなか出会えないものです。 自己努力をするとしても、一体何から着手すれば良いのでしょうか?

あなたが快適な開発者ライフを送るために、 あなたはいったい何を習得すれば良いのでしょう?

本書は、これ以上バグを出しなくない、もっとエレガントなコードだけ書いていたい、 快適な開発者ライフを送りたい、と願う開発者に贈るスキルアップのガイドラインです。

本書は、C++言語と、ソフトウェア設計に欠かせないデザインパターンについて 知識を習得するためのガイドラインです。

C言語をある程度マスターしている開発者を対象としています。

本書は、筆者自身が学習した結果から、 より適した順序で必要なことを学ぶ方法を考え記述しています。

そのため、本書では随所随所でいくつかの推薦図書を紹介しています。 それらの本はあなたがパワフルな開発者になるための必読書ではありますが、 すべて読み終えてからでなければ本書を読み進めることが 出来ないというわけではありません。

一番適した学習方法というものは人それぞれであり、ここに記した学習方法が あなたにとって最良の学習方法であるという保証はどこにもありませんが、 本書が、あなたをより快適にするためのスキルアップのきっかけになれば幸いです。

設計スキルとコーディングスキル

まず、忙しいあなたのために、一番お勧めの学習方法を手短に記しておきましょう。

「デザインパターン」という言葉を知らなかったり、 それについて理解が不十分であると考えているならば、 真っ先に以下の書籍を購入して読破することをお勧めします。

  • ─ デザインパターンとともに学ぶ ─ オブジェクト指向のこころ
    • アラン・シャロウェイ+ジェームズ・R・トロット=著
    • isbn:4894716844

しかし、いきなりそう言われてもあなたは納得しないかもしれません。

デザインパターンについて今すぐ学ぶことがあなたにとって 快適な開発者ライフを送るために重要なことであると、 あなたは考えていないかもしれないからです。

その場合は、本書を読み進めて 習得したいと思える事柄を見つけてまずそこから学習しても良いかもしれません。

快適な開発と保守をするためには、適切な設計を行うためのスキルと、 それをより適切に実現するためのコーディングスキルの両方が必要になると 筆者は考えています。

どちらが欠けても不幸なバグやストレスを伴う保守作業が発生するリスクを負います。

どちらから学習したとしても、最終的にはどちらも習得しなければならないのですから、 意欲が沸く方から先に学習しても問題ないでしょう。

筆者の場合も、最初は上記の書籍から読んだわけではなく、 C++についての学習を先に実施しました。

しかし、すべての書籍を読んでみた結果、 まず何よりも先にデザインパターンとオブジェクト指向について、 確固たる知識を得ておいた方が、学習中に数々の罠(=誤った設計に基づいた実装: 開発者本人はとても優れた設計に基づいた実装であると誤解している) に陥ることが少ないのではないかと考えるようになりました。 また、その後の全ての学習がスムーズであるとも思います。

デザインパターンは革命です。 あなたが持つオブジェクト指向の適応方法についての概念を 革新的に変えることになるかもしれません。

ですので、書籍「オブジェクト指向のこころ」は、 オブジェクト指向についての理解が十分であるとあなたが考えていたとしても、 あるいは、オブジェクト指向についての理解が不十分であっても、 まずはじめに読んでみることをお勧めします。

書籍「オブジェクト指向のこころ」を読むと、以下のことがあなたにもたらされます。

  • デザインパターンを知ることでオブジェクト指向が理解しやすくなる。
  • 最低限必要なUMLの表記方法を知ることができ、それに慣れ親しむことができる。
  • あなたのソフトウェア設計にデザインパターンを活かすことができるようになり、 優れた設計をすることが容易になる。
  • 誤った設計の事例を知ることができ、 あなたの設計でそれを避けることができるようになる。

以上のように、書籍「オブジェクト指向のこころ」を読んで、 「デザインパターン」についてまず学ぶことをお勧めします。

デザインパターンは、革命的であり、今なおとてもホットな概念です。

不幸にも、このお勧めに興味が沸かなかったり、 どうしてもコーディングスキルを先に磨きたいと思ってしまう場合は、 この先を読み進めて、別なところから学習に着手していきましょう。

この後の章では、 コーディングスキルの向上に関する話題からまず記述し、 その後、デザインパターンとその実装方法についての話題を展開してゆきます。

どちらから学ぶにせよ、確かな設計スキルと、 それを適切に実現するための確かなコーディングスキルの両方を習得することで、 より快適な開発と保守を現実にすることができるです。

21世紀的なコーディングをするために

コードを書けば書くほどバグが発生するリスクが増えます。 開発を管理する側の立場の人々は、 概ね、バグはステップ数に比例して増えていくという見解を持っています。 このアイデアを元にした行き過ぎの管理はうまくいかないことがしばしばありますが、 見解自体は的を得ていると思います。 そう、コードを書けば書くほど、バグが発生するのです。

プログラマは、コードを書いてはテストをしバグを検出して修正を入れます。 そしてテストをすると、修正のためのコーディングによりさらにバグが発生し その改修に追われます。

こうして出来上がったプログラムに対し、変更の要求が発生すると、 プログラマは該当箇所を見つけるために大域検索をおこない、 見つけた箇所に変更を入れます。 そしてテストをすると、変更のためのコーディングでバグが入り込み、 その改修をしてはテストをし、さらに入り込んだ新たなバグの改修に追われます。 これが現実的なプログラマの仕事です。

理想的な世界では、開発者はほとんどコーディングをしません。 コーディング量が少なければ少ないほど、そこにバグが入り込む余地がなくなるのです。 コードを書かないことがバグを減らす秘訣なのです。 開発者はもはやプログラミングとバグ改修の職を失い、 ソフトウェア設計やテスト設計を行うデザインの仕事が中心となります。

理想的な世界では、開発者は提示された要求を分析し、 その要求を満たすためのソフトウェアをデザインします。 いくつかの良く知られたデザインパターンとイディオムを随所に適応し、 要求を満たせるソフトウェアのロジックを作り上げます。 そして、そのロジックを実装するための必要最小限のコードを書きます。

コーディングはただ単に、ライブラリによって提供される デザインパターンやイディオムのパーツをロジックどおりに用いて、 各パーツにパラメータを設定し、各パーツを結びつける作業をするだけです。

こうして出来たソフトウェアは、ライブラリによって提供されるデバッグ機能で そのままでもある程度デバッグが行えるようになっています。 エンジニアは最後の仕上げとしてテスト仕様を決め、 テストを自動化するためのコードを仕様どおりに実装します。

1クリック(またはコマンドラインでmakeと入力)するだけで、ビルドが始まり 実行ファイルが作成されます。

1クリック(またはコマンドラインで make testと入力)するだけで、 すべてのテストが自動的に行われ、結果がまとめられます。

ソフトウェアの仕様変更が生じた際は、 エンジニアは設計を見直しロジックを変更します。 変更箇所に相当するテスト仕様を書き起こし、ロジックに誤りがないことを確実にします。 エンジニアはロジックを変更した箇所に相当するコードと、テスト実装を わずかなコードの中から見つけて、変更したロジックどおりに修正します。

そして1クリックでビルド、1クリックでテストが完了し、作業が終わります。 これがバグのない世界でのエンジニアの仕事です。

こんなことが本当に実現できるのでしょうか? 答えはYESです。しかしそれにはチーム内の開発者のベーススキルアップが求められます。 チーム内の開発者は次のようなことを理解し、共通認識を持つ必要があります。

  • プログラム言語仕様の基本的な理解
  • イディオムやコーディングテクニック(うまくいかないコードの定石とうまくいくコードの定石)
  • デザインパターン(うまくいく設計の定石)
  • イディオム、デザインパターンをコードに適応する方法、チーム内での取り決め。

スキルアップしよう

C言語を使う開発者は圧倒的に多いですが、 昨今ではC言語だけでは多様な要求に耐えられなくなってきています。 C言語は、あまりにも基本的なマシン処理手続きを表現した言語であるため、 ソフトウェア設計レベルの単純な「デザイン」を実装するだけでも、 多くのコーディングが必要となってしまうのです。

様々な言語がありそれぞれ異なる目的を持って設計されており、一長一短です。 C言語以外のプログラミング言語の選択の余地は十分あります。 プロジェクトの目的に応じて選択が必要です。

  • C++, JAVA, C#, J#, AspectJ, Python, Ruby

ツール作成に適したスクリプト言語、 将来有望かもしれないアスペクト指向言語などありますが、 ここではC言語のコンセプトをより多く引き継いでいるC++について取り上げ、 スキルアップのガイドラインを示します。

C言語とC++では一体何が違うのでしょうか?

C言語はC++の25%

C言語を用いた場合、プログラマはマシンの処理を記述して行かなければなりません。 ソフトウェアデザインをマシンの処理手順として記述していくには、 開発者自身が多くのコードを考えコーディングする作業が必要になるのです。

C++やJAVAなど、オブジェクト指向型言語では、これらの開発者の仕事を コンパイラが代わりにやってくれるのです。

開発者は、ソフトウェアデザインをそのまま表現したコードを記述するだけで良いのです。 そしてコンパイルすると、開発者が明示的に記述していない多くの処理手続きを、 コンパイラが生成してくれるのです。

C++が提供する機能は大きく分けて4つあります。 C++はC言語にはない重要な驚くべき機能を提供しています。

  • C言語としてのC++
    • C++はC言語と同じように記述することもできます。C++にはC言語の部分があるのです。
  • オブジェクト指向言語としてのC++
    • クラス、カプセル化、継承、ポリモーフィズムなど、 古典的なオブジェクト指向をデザインするための「クラス付きのC言語」を提供します。
  • ジェネリックプログラミングとしてのC++
    • テンプレートメタプログラミングという全く新しいプログラミング分野を生み出した 「テンプレート」という機能を提供します。
  • STL(標準テンプレートライブラリ)
    • STLは標準で提供されるC++のライブラリです。 コンテナ、イテレータと呼ばれるデータ構造や、 それらを処理するアルゴリズムなど、多くの汎用的な要素が提供されています。

C言語をたとえ100%使いこなしている開発者だとしても、 それはC++の機能の25%を使っているだけに過ぎないのです。

また、C++は今もなお進化中の言語です。ここ10年の間に大きく変化しました。 様々な試行や模索が行われ、混乱期を経て成熟し、 ようやく理想的なプログラミングをするノウハウと、 それをサポートする基盤ができてきました。 2009年にはC++の新しい標準仕様が制定される予定です。 この新しい仕様は現在C++0X,あるいはC++09と呼ばれています。 2009年まで待つ必要はありません。現在でも一部の機能をすぐに利用できます。 std::tr1 が利用できる環境であれば積極的に利用しましょう。 あるいは boostライブラリの利用を検討すると良いでしょう。

熟練のC言語プログラマも気持ちを一新してC++を学び直す必要があります。

特にテンプレートのプログラミングは、 C言語やオブジェクト指向言語としてのC++とは大きく異なり 開発者を惑わすことになるかもしれませんが、 一度正しいテンプレートを書き上げてしまえば、使うのは簡単です。

テンプレートは、いろいろな場面に何度でも使いまわせるような 汎用的な枠組みやアルゴリズムのユーティリティとして提供することができます。 開発者はテンプレートを利用することで、ほとんどコーディングすることなく、 コンパイラに大量の処理を自動生成させることができるようになります。

テンプレートを利用すると、コードを圧倒的に短くすることができるのです。

STLは、標準で提供されるテンプレートライブラリです。 標準で提供されたライブラリを使用することによって、 C++を使う開発者全員が実装方法の共通認識を持つことができ、 他人の書いたコードがよく理解でき、保守性を向上させることができます。

C++学習ステップのガイドライン

まずC++の基本的な理解を習得する必要があります。

筆者の場合は「C++の設計と進化」「プログラミング言語C++第3版」から入りましたが、 これはお勧めしません。 あえて難解な書籍から入らなくても、 まず簡単な書籍で概要を押さえておくのがスムーズな理解につながると思います。

筆者は読んでいませんが、以下の書籍が良さそうという話を聞きます。 amazonなどで評価の高い書籍を選ぶと良いでしょう。

C++で何ができるのか、ある一定のイメージがつかめたら 次はコーディングテクニックを身に着けなければなりません。

C++を使いこなすには、何ができるかという知識だけでは不十分なのです。 C++には、してはいけないことがたくさんあります。 C++で何をしてはいけないかを知り、 どのように書けばうまく行くのかというイディオムやテクニックを習得するには 以下の書籍がお勧めです。

次に、C++を使う開発者全員の共通認識と成り、 保守性を高めるために有用なSTLの使い方について知識を深めると良いでしょう。

STLの使い方には癖があり、特にコンテナの選択や、イテレータの操作を誤ると、 実行時効率をひどく落としたり、実行時エラーを引き起こす原因となります。 しかし、STLは標準なのです。 一度使い方をマスターすれば、プロジェクトが変わっても使うことができ、 C++を使う世界中の開発者同士で共通認識を持つことができ、 保守性を向上することができます。 コーディング量を減らし、コードの理解を助ける重要なツールです。

STLの使い方を習得するには以下の書籍がお勧めです。

C++習得への道はこれでは終わりません。 C++には、してはいけないことが多すぎるのです。 時間が許すのであれば、以下の書籍も読破しておくのがお勧めです。 上記のEffective C++とEffective STLで習得するであろう知識量よりは、 少ないかもしれませんが、見逃せない重要な情報が書かれています。

ここまででやっとコーディングスキルを習得することが出来ました。 これで、うっかり悪気もなく誤ったコーディングをして、 謎の実行時エラーと永遠に格闘するということはなくなるでしょう。

これですべて終わりでしょうか? いいえ、まだやるべきことが残されています。 大変でしょうか? いいえ、そんなことはないはずです。

バグ改修の負のスパイラルの中で永遠に翻弄され続けるよりも、 今学習して、このあと理想的な世界で快適に仕事をする方が、 楽に決まっているでしょう?

本節では、C++のコーディングスキルを身につけるために必読の書籍を紹介しましたが、 最後に、C++の確かな知識を得るためにいつでも参照できるよう手元において 補助的に使うと良さそうな書籍を紹介しておきます。

  • プログラミング言語C++
    • 言わずと知れたC++の生みの親 Bjarne StroustrupによるC++言語仕様の解説本です。
    • isbn:475611895X
  • C++ Coding Standards
    • コーディングルールの策定に迷ったら必要な箇所だけ参考にするのが良いでしょう。
    • isbn:4894716860

ソフトウェアデザインの学習と、C++での適応方法の習得

コーディングテクニックがある程度身に付いたら、 次はソフトウェアの設計手法を学習すると良いでしょう。 適切な設計を選択することで、不要なコーディングを避け、 柔軟性が高いソフトウェアを製造することができるようになるのです。

ソフトウェアデザインは天才でなければできないというわけではありません。 天才的な先人たちが発明したいくつかのデザインパターンを組み合わせることで、 エレガントでクオリティの高いソフトウェアデザインを容易に実現することが出来ます。

どのようなデザインパターンがあるのか、知識の差がものを言います。 まずは知ることです。

「イディオム」、「デザインパターン」というのは ソフトウェアデザインで使われる用語です。

イディオムはよりコーディングテクニックに近い概念です。

デザインパターンはそれよりも少しシステム設計に近い概念であり、 問題解決手法として先人たちが編み出したオブジェクト間の関係や相互作用の いくつかのパターンに名前をつけてカタログ化したものです。

開発者は自分が開発しようとしているソフトウェアの問題を解決するために 使えるパターンをこのカタログからいくつか選び、組み合わせて適応することで クオリティが高いソフトウェアデザインを行うことが出来ます。

デザインパターンという用語は、GoFと呼ばれる4人のシステムエンジニア達によって 執筆された書籍「オブジェクト指向における再利用のためのデザインパターン」において はじめてソフトウェア開発業界に持ち込まれました。

彼らはこの書籍の中で23個のデザインパターンについて紹介しました。 この23個のパターンの概要ついては、Wikipediaで概要を知ることが出来ます。

Web上にはデザインパターンについてさまざまな情報が掲載されていますが、 検索上位にヒットするページの中にも、 間違った理解を元に解説したり独自の実装例の紹介をしている人もいます。 どこかで講師をしておられる方のデザインパターン概要の連載記事も 誤った理解を元に堂々と解説をされております。 参考までに留めて鵜呑みにせず 評価の高い書籍を購入してきちんと学習するのが良いと思います。

本書の「設計スキルとコーディングスキル」の章でも紹介した書籍 「オブジェクト指向のこころ」を読むことを再度お勧めします。

しかし、デザインパターンの概念だけ知ったところで、それをどう実装すれば良いのか いまいちピンとこないことも多いかもしれません。 良いソフトウェアデザインができたところで、それをその通り実装する術を知らなければ 結局プログラムデザインやコーディングでつまづき、 開発者は再びバグスパイラルに巻き込まれてしまうのです。

ここではまずより実践的に、有用性の高いデザインパターンから順番に、 実装と結びつけて理解を深めていく学習方法を紹介しましょう。

C++でデザインパターンを適応するための知識を学習するには、 以下の書籍がおすすめです。 この書籍はデザインパターンは初心者だが C++のコーディングテクニックは概ね理解したはずである、 という前述のEffectiveシリーズを読破したくらいのレベルの開発者に うってつけの知識を与えてくれるものと思われます。

この本の前半では主に、 C++のテンプレートメタプログラミングについてのテクニックを紹介しています。 デザインパターンや、ジェネリックプログラミングをC++で実装するには、 テンプレートメタプログラミングが必要不可欠なのです。

テンプレートメタプログラミングは複雑怪奇であり、逃げ出したくなるかもしれませんが この本を読めば、簡単なテンプレートを作成するための 必要最低限の知識が得られるでしょう。

しかし、テンプレートがなにをもたらしてくれるのかという知識と、 必要に迫られた時に簡単なテンプレートが書けるだけの最低限の知識があれば それでいいのです。

なぜなら、これから行こうとしている理想的な世界では、 開発者はほとんどコードを書く必要はないのですから。 そこでは、テンプレート自体もほとんど書く必要はないのです。

必要なテンプレートは既に誰もがフリーで利用できるライブラリで提供されていたり、 各自のプロジェクトの開発指針に沿うようカスタマイズする際は、 テンプレートメタプログラミングをよりよく理解したチーム内メンバに 1度だけ書いてもらえばそれで良いのです。

うまく作られたテンプレートは1度だけ書けば、 それを使う側がテンプレートパラメータを少し変えるだけで、 驚くべき多様な実装を生み出します。

本の後半では、デザインパターンのうち 以下のパターンについて解説と実装を紹介しています。

  • Commandパターン
  • Singletonパターン
  • Factory Methodパターン
  • Abstruct Factoryパターン
  • Visitorパターン

簡単に各デザインパターンを紹介しておきます。

Commandは別名ファンクタと呼ばれるものです。 あるメソッドの呼び出しに必要なインスタンスやパラメータのセットと、 メソッドへの関数ポインタ、もしくはメソッド自体を含んだオブジェクトです。 これは、ある関数を現在のコンテキストとは別の別のコンテキストで 実行させることができるなど、強力な使い道があります。 しかし、今日ではstd::tr1::functionを使って同じことが実現できます。 ファンクタの実装については「Modern C++ Design」のものよりも std::tr1::functionを使用すると良いでしょう。

  1. #include <tr1/functional> // for Command pattern
  2. //
  3. // Command Pattern
  4. //
  5. using std::tr1::function;
  6. using namespace std::tr1::placeholders;
  7. using std::tr1::bind;
  8. // Usage:
  9. // void func( int a, int b, double c );
  10. // class FooClass{
  11. // void MemberFunc( int a );
  12. // };
  13. //
  14. // function<void (int, int, double )> cmd1( func );
  15. // cmd1(5, 10, 2.5);
  16. // function<void (int, double)> cmd2( bind(cmd1, 5, _1, _2 ) );
  17. // cmd2(10, 2.5); // same as cmd1(5, 10, 2.5);
  18. // FooClass foo;
  19. // function<void (int)> cmd3( bind( &FooClass::MemberFunc, &foo, _1 ) );
  20. // cmd3(30); // same as foo.MemberFunc(30);

Singletonパターンは、そのソフトウェアの実行中に、 ある具象クラスのインスタンスが唯一であることを保証するために 利用するデザインパターンです。 「Modern C++ Design」では、ソフトウェア終了時のあらゆる微妙なケースに対応できる 完全な実装例を示すために解説がやや長くなっていますが、 そこまで考慮しなくて良い場合、もっと単純な実装で十分です。

  1. #define IMPLEMENT_SINGLETON_CLASS(ConcreteClass) \
  2. public: \
  3. static ConcreteClass& Instance() \
  4. { \
  5. static ConcreteClass instance; \
  6. return instance; \
  7. } \
  8. protected: \
  9. ConcreteClass(); \
  10. ~ConcreteClass(); \
  11. private: \
  12. ConcreteClass( const ConcreteClass& ); \
  13. ConcreteClass& operator=( const ConcreteClass& )
  14. // 以下の様に、具象クラス内で使用する。
  15. // class FooConcrete {
  16. // IMPLEMENT_SINGLETON_CLASS(FooConcrete);
  17. // };
  18. // FooConcrete::FooConcrete()
  19. // {
  20. // // コンストラクタを実装
  21. // }
  22. // FooConcrete::~FooConcrete()
  23. // {
  24. // // デストラクタを実装
  25. // }

上記で示したマクロも、「Modern C++ Design」やBoostで提供する Singletonクラステンプレートと同様に不完全なものです。 たとえば引数を取るコンストラクタを使って生成する必要があるシングルトンクラスの 場合には、上記マクロは使えません。 マクロの代わりに、マクロの内容を少し変えたコードを 自分で対象の具象クラスに追加する必要があります。

しかし、どのSingletonクラステンプレートも多かれ少なかれ似たような 欠陥があるのです。 Singletonは概念が単純な割には、完全に汎用的かつすっきりとした実装をするのが難しいのです。

上記のマクロも、厳密には汎用的ではありませんし、 「Modern C++ Design」で述べているような消滅の順序の指定もできません。 しかし、そのような考慮が不要である多くの場合に、 手軽にSingletonを実装できるものなのです。

どのSingletonも一長一短です。

例えば「Modern C++ Design」で述べられているSingletonHolderは、 SingletonHolderクラスの定義と共に、 具象クラスにも手を加える必要がありシングルトン用の変更箇所が 2箇所に別れるためすっきりしません。

上記マクロではSingletonクラスを継承したり保持しているわけではありませんので、 Doxygenなどのソース解析ツールを使ってクラス図を自動生成させた場合、 その具象クラスがSingletonであるということがクラス図上で明示されません。

しかしqutilプロジェクトでは簡単に使用でき軽量な上記マクロを採用することにします。

Factory Methodは、ある抽象クラスを継承した数種類の具象クラスを、 パラメータに応じて生成し分けるメソッドを実装しなければならないとき、 switchなどで分岐して各具象クラスを生成する処理をコーディングする代わりに もっと柔軟性の高い方法で実装することを可能にするデザインパターンです。 このようなケースは割と多く、有用性が高いデザインパターンです。

Abstruct Factoryパターンは、実行時のモードによって、 生成するインスタンスの組み合わせを変えたい時に有用なデザインパターンです。 そのようなケースがあるか無いかはプロジェクトによってまちまちだと思いますが、 決して稀なケースではないと思います。 このような実装方法があるということは知っておいた方が良いでしょう。

Visitorパターンは、開発後に新規機能を追加する変更が入った際などに、 利用することができないか検討する価値があるデザインパターンです。 ある抽象クラスを継承した複数の具象クラスのうち、一部の具象クラスにのみ 新規の機能を追加したい場合などに有効です。 抽象クラスや対象のクラスに大きな変更を加えることなく、 新しい機能を付加することが出来ます。

また、この本の著者は、フリーで使えるテンプレートライブラリLokiを公開しています。 本で紹介されているいくつかの実装を利用したい場合は、Lokiを利用すると良いでしょう。

しかしながら、Lokiはエレガントなライブラリではありますが、 今日では、よりメジャーなBoostライブラリが Lokiが世に示したテンプレートメタプログラミングの思想を受け継ぎ、 さらに高機能なテンプレートメタプログラミングのためのパーツを提供しています。

あなたがLokiを使ってやりたいことがBoostでより簡単に実現できないか Boostのテンプレートメタプログラミングについて調べてみると良いでしょう。

Boostライブラリについては後述します。

理想的なソフトウェアデザインのためのデザインパターンとその実装例

さて、理想的なソフトウェアデザインをするためには、 もう少し他のパターンも知っておく必要があります。

それは、MediatorパターンとObserverパターンです。

Mediatorパターン

Mediatorパターンは、複数のオブジェクト間の通信を仲介し、 オブジェクト間の関係を簡潔にするために使われるデザインパターンです。

 +----------------------------------------+
 |                Mediator                |
 +----------------------------------------+
   ↑          ↑          ↑          ↑
   ↓          ↓          ↓          ↓
 +----+      +----+      +----+      +----+
 |ObjA|      |ObjB|      |ObjC|      |ObjD|
 +----+      +----+      +----+      +----+

Mediatorパターンは少しオブジェクト指向的な概念ではないと思われがちな デザインパターンです。 このため注意してプログラム設計を行わないとオブジェクト指向になれた開発者は 誤った実装をしてしまうかもしれません。

概念ばかり先行してMediatorパターンを採用したソフトウェアデザインをするのは 容易ですが、プログラム設計や実装で選択を誤ると プロジェクトを不幸な結果へと招くでしょう。 過去に、このような概念を導入してプロジェクトが失敗したことがある人は、 Mediatorパターンを避けようとするかもしれません。

しかしMediatorパターンを避けることはないのです。 実装方法を変えるという選択肢を、 テンプレートメタプログラミングが提供してくれます。

Mediatorを使うと、 各オブジェクトは通信先のオブジェクトを明示的に参照する必要がなくなります。

つまり、ObjDが提供する機能を使ってある処理を実現させたいObjAが、 ObjDのポインタや参照を持つ必要がなくなります。 このことは保守性の向上にとって重要です。

なぜなら、将来仕様変更が入った際に、 ObjDではなく、ObjBとObjCの機能を使ってある処理を実現させる必要ができた場合、 もしObjAがObjDの参照かポインタを保持していたとしたら、 ObjAのクラス定義自体を修正する必要があるのです。 すると、ObjAにObjBとObjCのインスタンスを知らせる仕組みを 導入する必要も出てきます。 ObjAのコンストラクタの引数を変えたり、setObjB(),setObjC()のようなメソッドを ObjAが提供しなければなりません。 また、ObjAのそのメソッドを呼ぶ側の処理も修正しなければならないでしょう。

これは、「ObjDではなく、ObjBとObjCの機能を使ってある処理を実現させる」という 仕様変更の文章が短いのに対して、 随分長々といろいろな修正を加えていなければいけないと思いませんか?

Mediatorを使えば、ObjAからのある要求に対応する関数だけを変更し、 ObjDに要求を出す代わりにObjBとObjCに要求を出して、 ObjAに結果を返すようにすれば良いのです。 仕様変更の文章と、ほぼ一対一の変更をソースに加えれば良いだけです。

Mediatorを、例えばOSが提供する::send()のようなプロセス間通信の機能や、 非同期通信を提供するスレッドマネージャのようなものと考える人も いるかもしれませんが、それは誤解です。

例えば、::send()では、通信先相手を指定する必要があります。 それが単なる参照やポインタでなく、ファイルパスやIPアドレス、 あるいは::connect()されたファイルディスクリプタだったとしても、 明示的に通信相手を指定しているのです。

  1. vector<char> buf;
  2. int fd = socket(...);
  3. // connect( fd, 宛先 )する
  4. ::send( fd, &buf, buf.size(), 0 );

非明示ということは、通信先相手を示すようなパラメータを全く渡すことなく、 要求に必要な最低限のパラメータのみを渡すメソッドとして、 Mediatorを実装することが考えられます。

  1. SomeResult result
  2. = Mediator::Instance().Send( SomeRequest( PARAM1, PARAM2, ... ) );
  3. // SomeRequestには宛先情報が含まれていない。SomeResutは任意の型。

つまり、Mediatorは宛先を指定しない複数のAPIを提供する関数群というように 理解することができます。 (もちろん他の実装方法も考えられますが、ここではそのように扱って実装します)

Mediatorから要求を受け取るオブジェクトはすべてColleagueと呼ばれます。 ColleagueはNotify()というメソッドを提供しており、 Mediatorによって転送されてきた要求をこのメソッドで受け取り、 処理を行い結果を返します。

ここで、Mediator::Send()の実装について考えて見ましょう。 Mediator::Send()を単一の関数として実装した場合、 3つの弊害が考えられます。

1つ目の弊害は、処理効率の低下です。 すべての要求が単一のMediator::Send()に集まってくると、Mediator::Send()内では、 再びそれぞれの要求に応じた処理に分岐するためディスパッチしなければなりません。 本来、単に別オブジェクトのメソッドをコールするだけのオーバーヘッドで良いはずの 処理すべてに、ディスパッチ処理のオーバーヘッドを追加するのは、 塵も積もれば山となる的に処理効率を低下させます。

2つ目の弊害は、未実装のディスパッチ先があった場合、 コンパイル時に検出できないことです。 Mediatorはすべてのオブジェクトを知っているという役割り上、 Mediator::Send()の中でswitch()でディスパッチするのも一つの手です。 あるいは、std::mapなどを使い、対数ディスパッチャなどを実装するかもしれません。

しかし、いずれの場合も、ある要求がMediator側で未実装だった場合、 呼び出し側のMediator::Send()はコンパイルが通ってしてしまうのです。 この場合、実行時にswitch()のdefault:の処理が実行されることになるでしょう。 仮にそこにassert(0)やログ出力などが書かれていたとしても、 開発者が未実装に気づくタイミングは随分後になってからということになります。

3つ目の弊害は、関数の戻り型がすべて同じになってしまうことです。 ある要求ではboolを返すのが妥当だとしても、別の要求ではvoidで良いかもしれません。 場合によっては、全く違った独自の型を返して欲しいこともあるでしょう。

単一のMediator::Send()でこのようなことを実現するには、 戻り値の代わりとなる結果の格納先のポインタをパラメータとして渡す必要があります。 しかしここで厳格なタイプシステムを破壊してしまうのです。

  1. // 結果はvoidで良い場合
  2. Mediator::Send( Request3Param( PARAM1, PARAM2, ... ), 0 );
  3. // boolの結果を返して欲しい場合
  4. bool result1;
  5. Mediator::Send( Request1Param( PARAM1, PARAM2, ... ), &result1 );
  6. // 独自の型に結果を格納して欲しい場合
  7. Request2Result result2;
  8. Mediator::Send( Request2Param( PARAM1, PARAM2, ... ), &result2 );

Mediator::Send()が単一である場合、 上記のようなコードがコンパイルエラーとならないためには、 Mediator::Send()は以下の様にvoid*を引数として受け取るか、 あるいは醜悪な可変長引数 '...' を受け取るよう宣言されていることでしょう。

  1. class Mediator{
  2. Send( const RequestParam& param, void* result );
  3. };

すると、本来 Request2Result*を渡さねばならない要求に対して、 呼び出し側で誤ってbool*を渡してしまうとどうなるでしょう。 結果は未定義のメモリ破壊となります。 このようなエラーはコンパイル時には検出されず、実行時にもなかなか原因がわからない やっかいなバグにつながります。 このような問題をコンパイル時に検出するためには、 C++が持つ厳格なタイプシステムを破壊してはならないのです。

ではどう実装すれば良いのでしょう?

もうお気づきだと思いますが、Mediator::Send()を単一でなく 複数実装すれば良いのです。

複数の要求があるのですから、複数のMediator::Send()があれば、 ディスパッチの必要はありません。 コンパイラが自動的に適切なMediator::Send()を選び、要求元の箇所に、 選択したMediator::Send()を呼び出すよう処理を埋め込むのです。 ディスパッチはコンパイル時に完了しており、実行時の処理効率を落としません。

また、ある要求に対するMediator::Send()が未実装だった場合は、 コンパイルエラーとなって検出できます。

空関数を定義してやればコンパイルを通すこともできますが、 その要求専用の空関数が定義されることで、 そこに何かを実装しなければいけないということが明確になるのです。

caseを丸ごと何も書かなくてもコンパイルが通るswitch()とは異なり、 他から使われている必要な処理がなければ、 まずコンパイルエラーとして検出されるわけです。 このことは、仕様漏れや考慮漏れにつながるような危険な実装忘れを 早期に発見するということにつながります。

複数のMediator::Send()を実装する、 つまりMediator::Sendをオーバーライドするということは、 戻り値の型を要求毎に変えることも可能になります。 するともはや、タイプシステムを破壊するような汎用ポインタの引数渡しは不要となり、 呼び出し元で不正な型を渡すとコンパイルエラーとして検出されるようになるわけです。

具体的に説明しましょう。 Mediatorは以下の様に具象化することができます。

  1. // CoreMediator から Notify() するColleagueを登録する。
  2. // CoreMediatorへSend()するがCoreMediatorからNotify()しないColleagueは登録不要。
  3. typedef TYPELIST(7,
  4. MsgReceiver,
  5. IncomingMsgQueue,
  6. Worker,
  7. Worker,
  8. ConfigTable,
  9. MsgSender,
  10. MsgSender
  11. ) ColleagueList;
  12. enum ColleagueId{
  13. idMsgReceiver,
  14. idIncomingMsgQueue,
  15. idWorker1,
  16. idWorker2,
  17. idConfigTable,
  18. idMsgSender1,
  19. idMsgSender2
  20. };
  21. class CoreMediator: public Mediator<ColleagueList> {
  22. IMPLEMENT_SINGLETON_CLASS(CoreMediator);
  23. public:
  24. void Send( const MyMsg001_Forwarding& msg ) const
  25. {
  26. Field<idIncomingMsgQueue>(colleague_).ptr_->Notify( msg );
  27. }
  28. bool Send( const MyMsg002_ConfigWriteFoo& msg ) const
  29. {
  30. // ... 何らかの処理
  31. return Field<idConfigTable>(colleague_).ptr_->Notify( msg );
  32. }
  33. // ここに様々なSendを追加してゆく
  34. };
  35. inline CoreMediator::CoreMediator(){}
  36. inline CoreMediator::~CoreMediator(){}

ここでは詳細な説明は省きますが、 それでは気持ち悪くて理解の妨げになりますので簡単に説明しておきましょう。

TYPELISTは、「Modern C++ Design」で紹介されているLokiのタイプリストです。 書式が少し異なりますが、qutil内で、 使いやすいように書式を変えるマクロが組まれています。

ColleagueIdはTYPELISTで指定した順に、ColleagueのインスタンスのIDを定義しています。

class CoreMediatorというのが、 各ソフトウェアで固有に具象化するMediatorの具象クラスです。 Mediator<ColleagueList>というテンプレートクラスをpublic継承するだけで、 ColleagueListで指定されている型のフィールドをクラス内に保持するようになります。

各フィールドには、Field< ColleagueID >(colleague_)という 関数テンプレートでアクセスすることが出来ます。 Fieldについても、「Modern C++ Design」で紹介されています。 実行時に関数がコールされてフィールドが検索されるわけではなく、 コンパイル時にColleagueIDに対応したフィールドを参照するよう解決されます。

IMPLEMENT_SINGLETON_CLASS()は、 「Modern C++ Design」で紹介されているSingletonとは異なります。 qutil内で、マクロが定義されています。 Instance()というメソッドが実装され、 CoreMediatorがシングルトンとして利用できるようになります。

さて、注目すべきは、CoreMediator::Send()の実装です。 switch case あるいは if else if の連続とほぼ同じような手間で 各要求(msgのタイプ)毎のSendを実装することができます。 手間は同じで、こちらには分岐による実行時の処理負荷がありません。

コンパイルした時点で、すでに分岐済みのメソッドが呼ばれるように コードが生成されるのです。

CoreMediatorは面倒な保守しずらい処理を一切コーディングすることなく、 各要求に対する要求先を決定する役割りを果たすためのコードだけを 記述してゆけば良いのです。

Colleague側の具象化も簡単です。

  1. class CoreMediator;
  2. class IncomingMsgQueue: public Colleague<CoreMediator> {
  3. public:
  4. template<typename ID_Int2Type>
  5. IncomingMsgQueue( ID_Int2Type );
  6. void Notify( const MyMsg001_Forwarding& msg )
  7. {
  8. LOG( INFO, "001 %p", this );
  9. }
  10. // ここにIncomingMsgQueueが受け取るNotifyを要求(msgのタイプ)毎に記述してゆく。
  11. };
  12. template<typename ID_Int2Type>
  13. IncomingMsgQueue::IncomingMsgQueue( ID_Int2Type )
  14. {
  15. subject.SetObserver( *this, ID_Int2Type() );
  16. }

IncomingMsgQueueクラスがシングルトンの場合は、 コンストラクタをテンプレート関数にする必要はなく、 デフォルトコンストラクタ内で直接ColleagueIdを指定して実装することも出来ます。 しかし、ここでは説明のため汎用的にシングルトンではない形で実装しています。

IncomingMsgQueueのインスタンスを生成するコードは例えば以下の様になります。

  1. IncomingMsgQueue incomingMsgQueue( Int2Type<idIncomingMsgQueue>() );

これで、idIncomingMsgQueueというIDを持つ IncomingMsgQueueクラスのインスタンスが生成され、 インスタンスのポインタがCoreMediatorの Field<idIncomingMsgQueue>(colleague_).ptr_に登録されます。

この状態で、どこかのオブジェクトが、

  1. CoreMediator::Instance().Send( MyMsg001_Forwarding( ... ) );

というようにCoreMediatorのSend()を呼べば、 IncomingMsgQueue::Notify( const MyMsg001_Forwarding& )が呼ばれることになります。

このようなことを実現するためには、MediatorとColleagueを 以下のようなテンプレートクラスとして定義すれば良いでしょう。

  1. //
  2. // Mediator Pattern
  3. //
  4. using Loki::GenScatterHierarchy;
  5. using Loki::Field;
  6. template <typename TList>
  7. class Mediator {
  8. template <typename T>
  9. struct Pointer {
  10. T* ptr_;
  11. };
  12. protected:
  13. typedef GenScatterHierarchy< TList, Pointer > ColleagueField;
  14. ColleagueField colleague_;
  15. public:
  16. template< typename ID_Int2Type, typename ColleagueType >
  17. void SetColleague( ColleagueType& colleague, ID_Int2Type )
  18. {
  19. Field<ID_Int2Type::value>(colleague_).ptr_ = &colleague;
  20. }
  21. // Concrete class must implement Send() function each MessageType.
  22. // For example.
  23. // SomeResultType Send( const SomeMessageType& msg )
  24. // {
  25. // return Field< COLLEAGUE_ID >(colleague_).ptr_->notify( msg );
  26. // }
  27. };
  28. template <typename MediatorType>
  29. class Colleague{
  30. protected:
  31. MediatorType& mediator_;
  32. public:
  33. Colleague(): mediator_( MediatorType::Instance() ){}
  34. virtual ~Colleague(){}
  35. template<typename ResultType, typename MessageType>
  36. ResultType Send( const MessageType& msg )
  37. {
  38. return mediator_.Send( msg );
  39. }
  40. // Concrete class must implement Send() function each MessageType.
  41. // For example.
  42. // SomeResultType Notify( const SomeMessageType& msg )
  43. // {
  44. // ResultType result;
  45. // // doing something.
  46. // return result;
  47. // }
  48. };

ここで紹介したColleagueのテンプレートでは、 Mediatorがシングルトンであることを前提としています。 GoFのデザインパターンではMediatorが必ずシングルトンであるとは言っていませんが、 実際のソフトウェアデザインでは、実質上ほとんどのケースでMediatorは シングルトンになると思われます。

もしあなたのプロジェクトでMediatorがシングルトンではないケースを 扱う必要があるなら、Colleagueのコンストラクタを以下の様に Mediatorインスタンスを受け取るようにして、 Colleagueの具象クラスのコンストラクタでインスタンスを指定すると良いでしょう。

  1. Colleague( MediatorType& mediatorInstance ): mediator_( mediatorInstance ){}

Observerパターン

Observerパターンは、あるオブジェクトが変化した際に、 それを監視するオブジェクトすべてに通知が行われる仕組みを 汎用的に実装するためのノウハウです。

このパターンは、もちろん単発で使っても有効ですが、 MediatorにObserverパターンを保持させると 柔軟性が高いオブジェクト間通信フレームワークを作ることが出来ます。

例えば、ある種の要求がMediatorに対して発行された際に、 その要求によって影響を受けるすべてのオブジェクトに通知を行う必要があるとします。 この時、Observerパターンを使わずにMediatorを実装すると 極めて柔軟性がない実装となってしまいます。

  1. bool Send( const MyMsg002_ConfigWriteFoo& msg ) const
  2. {
  3. // ここで、この要求に関連するWorker1とWorker2に通知する。
  4. Field<idWorker1>(colleague_).ptr_->Notify( msg );
  5. Field<idWorker2>(colleague_).ptr_->Notify( msg );
  6. // ここから本来の処理を実行。
  7. return Field<idConfigTable>(colleague_).ptr_->Notify( msg );
  8. }

上記のように、Mediatorの中で通知処理を記述してしまうと、 もしもその要求を監視したいオブジェクトが増えたり変更された際に、 Mediatorを書き換えなければなりません。 これでは柔軟性がありません。

要求を監視したいのは、各オブジェクトの都合であり、 Mediatorを書き換えなければ監視できないというのは理想的ではありません。

各オブジェクトは、ある要求を監視するという処理のみ追加すれば監視できるようになり、 Mediatorとしては、「ある要求が発生したので監視しているオブジェクトに通知する」 という処理だけ実装して、 あとは勝手に個々のオブジェクトへ通知が行われるのが理想です。

このように実装するためには、Observerパターンが都合が良いのです。

Observerパターンでは、監視する側をObserverと呼びます。 通知する側をSubjectと呼びます。

Configの変更要求が行われた際に、関連するオブジェクトに通知を行う Subjectを定義してみましょう。

  1. // Config変更時に通知するオブジェクトを登録する。
  2. typedef TYPELIST(2,
  3. Worker,
  4. Worker
  5. ) ChangeConfigObserverList;
  6. class ChangeConfigCommunicator: public Subject<ChangeConfigObserverList> {
  7. public:
  8. enum ObserverId{
  9. Worker1,
  10. Worker2
  11. };
  12. };

次に、Mediatorの具象クラスであるCoreMediatorに、 設定変更要求を受信した際に通知するSubjectを保持させて見ましょう。

  1. class CoreMediator: public Mediator<ColleagueList> {
  2. // ...さっきと同じSingletonの実装
  3. // ...
  4. ChangeConfigCommunicator changeConfigCommunicator_;
  5. public:
  6. ChangeConfigCommunicator&
  7. getChangeConfigCommunicator()
  8. {
  9. return changeConfigCommunicator_;
  10. }
  11. // ...
  12. bool Send( const MyMsg002_ConfigWriteFoo& msg ) const
  13. {
  14. changeConfigCommunicator_.Notify( msg );
  15. return Field<idConfigTable>(colleague_).ptr_->Notify( msg );
  16. }
  17. };

CoreMediatorに、Config変更要求が行われた際に通知を行うSubjectを取得するための getChangeConfigCommunicator()を実装しています。

先ほどは、Worker1,Worker2のフィールドを直接参照していた CoreMediator::Send( const MyMsg002_ConfigWriteFoo& msg )が、 理想どおり、「ある要求が発生したので監視しているオブジェクトに通知する」 という処理のみ追加すれば良いようになりました。

Config変更要求が行われることを監視したいObserverの具象クラスであるWorkerの実装は 以下の様になります。

  1. class ChangeConfigCommunicator;
  2. class Worker: public Observer<ChangeConfigCommunicator>{
  3. public:
  4. template<typename ID_Int2Type>
  5. Worker( ChangeConfigCommunicator& subject, ID_Int2Type );
  6. void Notify( const MyMsg002_ConfigWriteFoo& msg )
  7. {
  8. LOG( INFO, "002 %p", this );
  9. }
  10. };
  11. template<typename ID_Int2Type>
  12. Worker::Worker( ChangeConfigCommunicator& subject, ID_Int2Type )
  13. {
  14. subject.SetObserver( this, ID_Int2Type() );
  15. }

Workerは以下の様にインスタンスを生成できます。

  1. Worker w1( CoreMediator::Instance().getChangeConfigCommunicator(),
  2. Int2Type<ChangeConfigCommunicator::Worker1>() );
  3. Worker w2( CoreMediator::Instance().getChangeConfigCommunicator(),
  4. Int2Type<ChangeConfigCommunicator::Worker2>() );

この状態で、どこかのオブジェクトが、

  1. CoreMediator::Instance().Send( MyMsg002_ConfigWriteFoo( ... ) );

というようにCoreMediatorのSend()を呼べば、 w1, w2それぞれのインスタンスに対して、 Worker::Notify( const MyMsg002_ConfigWriteFoo&)が呼ばれることになります。

ここで紹介した例を実現するためのSubjectとObserverのテンプレートクラスは、 以下の様になります。

  1. //
  2. // Observer Pattern
  3. //
  4. using Loki::Int2Type;
  5. template < unsigned int index >
  6. struct AllObserver {
  7. template< typename ObserverFieldType, typename MessageType >
  8. static inline void Notify( ObserverFieldType& observer,
  9. const MessageType& msg )
  10. {
  11. if( Field<index>(observer).ptr_ )
  12. {
  13. Field<index>(observer).ptr_->Notify( msg );
  14. }
  15. AllObserver< index - 1 >::Notify( observer, msg );
  16. }
  17. };
  18. template<>
  19. struct AllObserver< 0 > {
  20. template< typename ObserverFieldType, typename MessageType >
  21. static inline void Notify( ObserverFieldType& observer,
  22. const MessageType& msg )
  23. {
  24. if( Field<0>(observer).ptr_ )
  25. {
  26. Field<0>(observer).ptr_->Notify( msg );
  27. }
  28. }
  29. };
  30. template <typename TList>
  31. class Subject{
  32. template <typename T>
  33. struct Pointer {
  34. T* ptr_;
  35. };
  36. typedef GenScatterHierarchy< TList, Pointer > ObserverField;
  37. ObserverField observer_;
  38. public:
  39. template<typename MessageType>
  40. void Notify( const MessageType& msg ) const
  41. {
  42. AllObserver< Loki::TL::Length<TList>::value - 1 >::Notify( observer_,
  43. }
  44. template< typename ID_Int2Type, typename ObserverType >
  45. void SetObserver( ObserverType* observer, ID_Int2Type )
  46. {
  47. Field<ID_Int2Type::value>(observer_).ptr_ = observer;
  48. }
  49. };
  50. template< typename SubjectType >
  51. class Observer : private Uncopyable< Observer<SubjectType> >{
  52. protected:
  53. // Concrete class must implement constructor to the following.
  54. // For example.
  55. // template< typename ID_Int2Type >
  56. // ConcreteObserver( SubjectType& subject, ID_Int2Type )
  57. // {
  58. // subject.SetObserver( *this, ID_Int2Type() );
  59. // }
  60. public:
  61. // Concrete class must implement Notify() function each MessageType.
  62. // For example.
  63. // void Notify( const SomeMessageType& msg )
  64. // {
  65. // // doing something.
  66. // }
  67. };

しかしながら、ここで紹介したObserverの実装例は、 すべてのプロジェクトで有用であるとは限りません。 トレードオフがあるのです。

問題になると考えられるのは、ChangeConfigCommunicatorクラスで、 コンパイル時にObserverの型を指定しておかねばならないという点です。

もう一度定義をみてみましょう。

  1. // Config変更時に通知するオブジェクトを登録する。
  2. typedef TYPELIST(2,
  3. Worker,
  4. Worker
  5. ) ChangeConfigObserverList;
  6. class ChangeConfigCommunicator: public Subject<ChangeConfigObserverList> {
  7. public:
  8. enum ObserverId{
  9. Worker1,
  10. Worker2
  11. };
  12. };

Workerの型をタイプリストに設定したり、インスタンスのIDを定義しています。 これでは、 「各オブジェクトは、ある要求を監視するという処理のみ追加すれば監視できるようになる」 という理想に反していることになります。 Workerではないオブジェクトや、Workerの3番目のインスタンスが 要求を監視したくなった場合に、Mediatorは変えなくて良いものの、 ChangeConfigCommunicatorのタイプリストを変更したり、IDを追加しなくてはなりません。

さらに、実行時に、未知のオブジェクトが動的に監視を開始・停止したい場合は どうでしょう? この実装では対応できません。

この実装では、具象クラスの型をタイプリストに追加してテンプレートに渡すことで、 Observerの具象クラスとSubjectの具象クラスが強く結びつきすぎているのです。 動的に無制限に監視の開始・停止を行いたいケースには向いていないのです。

そのようなケースでは、テンプレートではなく、 通常のオブジェクト指向における継承を利用することで クラス間の結びつきを弱めることが出来ます。 Observer抽象クラスで、virtual関数のNotify()を実装すれば良いのです。

ただしその場合、Notify()のパラメータの引数の型の種類が複数ある場合は、 Notify内でディスパッチ処理を行わねばなりません。

virtual関数呼び出しの些細なオーバーヘッドと、 ディスパッチ処理による実装の手間と処理効率低下を省くため、 トレードオフの結果、ここではテンプレートを用いた実装を採用して例示しました。 あなたのプロジェクトでは別の選択をする必要に迫れれることもあるかもしれません。

Factory Methodパターン

Factory Methodパターンは オブジェクト指向で継承を使う場合に 検討してみる価値があるデザインパターンです。

ある抽象クラスを継承した数種類の具象クラスを、 パラメータに応じて生成し分けるメソッドを実装しなければならないとき、 switchなどで分岐して各具象クラスを生成する処理をコーディングしてしまうと 柔軟性が損なわれます。

新しいクラスの生成を追加する必要が生じたとき、caseを追加しなければなりませんし、 caseの追加を忘れた場合、コンパイル時にそれに気づくことが出来ません。

「Modern C++ Design」に詳しい説明があります。 そして、Lokiをそのまま利用するのが良いでしょう。

  1. #include "loki/Factory.h" // for Factory Method pattern
  2. //
  3. // Factory Method Pattern
  4. //
  5. using Loki::Factory;

なんと怠慢なコードでしょう。しかし驚くことはありません。 あなたがコードを書かなければ書かないほど、バグは発生しないのです。 ある程度信頼できるライブラリなら、利用できるものは利用すべきなのです。

しかし、ここではFactoryに登録する処理を自動化するための Registerテンプレートクラスを紹介しておきましょう。

  1. template< typename FactoryType >
  2. class Register: private Uncopyable< Register<FactoryType> >{
  3. public:
  4. template< typename IdType, typename CreateFunction>
  5. Register( IdType id, CreateFunction create )
  6. {
  7. bool result = FactoryType::Instance().Register( id, create );
  8. ASSERT( result != false );
  9. }
  10. };

このRegisterテンプレートクラスは、Factoryの具象クラスがシングルトンであることを 前提にしています。 Factoryは通常シングルトンであるのが理にかなっています。 ほとんどのケースでは Factory Methodとsingletonパターンを組み合わせて具象化させることが出来るでしょう。

Registerテンプレートクラスは以下の様に使用します。

  • XXX TODO: Registerテンプレートクラス使用例

このRegisterテンプレートクラスの利点は、初期化が自動化できることです。 Factory Methodへの登録処理を、独自の初期化処理に追加しなくても、 main関数が呼ばれる前の時点でRegisterテンプレートクラスのコンストラクタが呼ばれ 登録処理が完了するのです。

これを利用すると、オブジェクトファイルをリンクするだけで Factory Methodに機能を追加することができるようになり、 拡張性が高いFactory Methodを実装することができます。

ライブラリを積極的に利用しよう

  • STLを使いましょう
  • tr1とBoostを積極的に使いましょう
  • Lokiの利用を検討しましょう

前章で紹介した書籍を読破したみなさんなら、 すでにこれは当たり前のことと感じているかもしれません。

しかし、未だに自分の書いたコード以外は信じられない、 他人が書いたライブラリの使い方を覚える労力を払いたくないと 考えている方もいらっしゃるかもしれません。

もしそうなら、あなた自身のために、その考えを考え直した方が良いかもしれません。 この章が考え直すきっかけとなることを願います。

コードは書かなければ書かないほどバグを発生させないのです。 利用できるライブラリがあり、 そのライブラリが秀逸ならば利用しない手はないでしょう。

なぜBoostを使わないのですか?

もしかするとBoostの存在を知らない方も居るかもしれません。 ここでBoostについて触れておきましょう。 Boostがどのようなものか知った上で、少しでもあなたのプロジェクトに 役に立つ部分がありそうならば、Boostを利用することを検討すると良いでしょう。

筆者がBoost C++ライブラリの存在を知ったのは随分遅く、2006年に入ってからでした。

他社が試験的に開発したソフトウェアを解析する仕事があり、 そのソフトウェアがBoostを利用していたのです。

筆者の最初の反応は、拒絶反応です。 Boostは機能が豊富すぎるように思えました。 機能が豊富ということは、その使い方をマスターするために 様々な独自の使用方法を覚えなければならないということを意味するように思えたのです。

そして、せっかく使用方法を覚えたとしても、それは標準ではなく、 この先長い人生の中でもう2度と使うことのない 再利用できない知識となるのではないかと考え、そのようなことに労力を払うことを 避けたいと考えたわけです。

しかし、Boostについてもう少し知ると、その考えは変わりました。 Boostは標準に近い立場にあり、 しかもすべての機能を利用する必要はなく、使いたい機能だけ使えば良いのです。 そう、STLのように。

Boostのカバー範囲は広く、文字列処理や日付計算、ファイル操作、非同期通信、 数学的な演算、関数型プログラミング、メタプログラミングなどをサポートしています。

スレッドやファイル操作、IP通信、時刻取得などを システムコールを直接使用することなく利用できるのも魅力のひとつです。 これらの機能は通常システム依存ですが、 Boostを使えばLinux上でもWindows上でもコンパイルすることが可能です。

Boostは非常に高機能なライブラリです。 もしこれをあなたが独自に実装するとしたら、多くのコードを書かなければなりません。 すると、書けば書くほどバグを発生させるリスクを負うのです。 しかし、Boostを使えばそのリスクを一切負わずに、 強力な機能を利用することができるのです。

Boostは極めてメジャーなオープンソースプロジェクトであるため、 世界中の人々によって使用されており、 より多くのテストやレビューを無料で手に入れることが出来るのです。 あなたがユーティリティを自分でコーディングした場合は、 膨大な第三者の協力を無料で得ることは難しいでしょう。

Boostのすべてを覚える必要はありません。 あなたはあなたが必要とする機能を選び、その機能だけ使用することもできるのです。

以下の書籍が、Boostライブラリの非常に豊富な機能の概要と使い方をあなたに紹介する 良いカタログとなるでしょう。

ライブラリの寄せ集めに秩序を持たせるにはどうすれば良いのでしょうか

STLとtr1は標準です。 あなたのプロジェクトでこれらに対して不要なラッパーを定義すると、 誰もが共通認識できるせっかくの標準仕様を台無しにしてしまうかもしれません。

必要がない限り、できるだけSTLとtr1はラッパーせずに使ったほうが良いと思います。 STLの使い勝手は決して最高ではないかもしれませんが、 開発者は皆、標準仕様に慣れ親しむべきです。

経験が浅い開発者のコーディングミスを防止するために、 独自仕様のラッパーを苦心して作成し、その使い方を覚えさせる労力を払うよりも、 標準仕様のSTLとtr1の使い方を教育しましょう。

問題は、BoostとLokiです。 あるいはあなたのプロジェクトでは他のライブラリも採用するかもしれません。

これらは標準ではありませんし、 機能が豊富なためあなたのプロジェクトでは一部の機能しか利用しないかもしれません。

さらに、複数のライブラリが互いに似た機能を提供しており、 どちらを使うかは好み次第という状況も発生し得ます。

しかし、ライブラリを搭載してしまったなら、 あなたが使用するつもりがなかった機能をチーム内のメンバの誰かや、 後に保守作業を担当した開発者の誰かが何の悪気もなく使用してしまうかもしれません。

こうなるとコードはあなたが知らないうちに無秩序で保守しにくい悪魔へと変化して、 あなたを不幸へと引きずり込むかもしれません。 先手を打っておきたいところです。

コードに秩序を持たせ、あなたのプロジェクトで規範となるような方向性を示すには どのようにすれば良いでしょうか?

ここでもデザインパターンを適応して良い解決策が無いか考えてみましょう。 このようなケースに打ってつけのパターンがあります。 それはFacedeパターンです。

Facedeパターンは、複雑な既存実装の一部の機能だけを使用して、 より簡潔なインタフェースを提供するためのパターンです。

utility.png

Facedeパターンを利用したクラス(上図の「ユーティリティ」に相当する部分)に、 プロジェクトで利用すべき規範となるようなインタフェースを定義し、 そのメソッドの中から複数のライブラリを使い分けて使用すれば良いのです。

また、Facedeパターンには各オブジェクトのライブラリへの依存を無くすという うれしい副作用も効果もあります。 各オブジェクトは、複数のライブラリをあれこれ使い分けて使用する必要がなくなり、 Facedeオブジェクトとだけ関係を持てばよいことになります。

複数のライブラリの豊富な機能の一部を利用する場合、 Facedeパターンを利用して、秩序ある実装の方向付けを示すと良いでしょう。

どのようにプロジェクト専用のユーティリティを実装すれば良いでしょうか

qutilプロジェクトでは、上図の「ユーティリティ」に相当する部分を実装してゆきます。 提供しようとする機能は、キュー、スケジューラー、プロセス間通信、 デザインパターンテンプレート、デバッグログ出力、Sha-256、Uuid、 低次元データベース操作、タイマ機能、スレッド機能などです。

qutilは他のライブラリと同じく、 あなたのプロジェクトにとって必要ない機能も含まれるかもしれませんが、 実装の一例を示すものです。

利用できそうな機能がもしあれば利用を検討してみてください。