1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【書評】 C++ソフトウェア設計 ―高品質設計の原則とデザインパターン

1
Posted at

はじめに

Klaus Iglberger 著『C++ソフトウェア設計 ―高品質設計の原則とデザインパターン』(オライリー・ジャパン)を読みました。

C++ の本というと、新しい言語機能や細かな文法を扱うものが多い印象があります。本書はその逆で、言語機能の解説にはほとんどページを割かず、「依存関係をどう管理するか」という設計の話に集中しているのが特徴です。

この記事では、本書全体を1本にまとめた書評として、章ごとの内容と、読んで得られた考え方を整理します。設計やアーキテクチャに関心のあるエンジニアの方の参考になれば幸いです。

本記事は書籍の要約・感想であり、本文やコードを転載するものではありません。サンプルコードは、考え方を示すために筆者が書き起こした簡易なものです。詳細は書籍をご確認ください。

書籍の基本情報

項目 内容
書名 C++ソフトウェア設計 ―高品質設計の原則とデザインパターン
原題 C++ Software Design
著者 Klaus Iglberger
訳者 千住 治郎
出版社 オライリー・ジャパン
ISBN 978-4-8144-0045-4
構成 全11章・39のガイドライン

本書の立ち位置

著者は「プロジェクトの成否を分けるのは言語機能ではなく設計である」という立場をとっています。C++20 の機能(コンセプトなど)も登場しますが、あくまで設計を実現する手段として使われます。

読者として想定されているのは、継承やテンプレートの仕組みをすでに理解している C++ 経験者です。初心者向けの入門書ではない点には注意が必要です。

本書は、章ごとに1〜数個の「ガイドライン」を置く構成です。ガイドラインは独立して読み始めることもできますが、相互にクロスリファレンスされています。

全体を貫く5つの考え方

最後の章で著者自身がまとめている内容ですが、本書を通して繰り返し出てくるのは次の5点です。

  1. 依存関係を最小化する
  2. 関心を分離する
  3. 継承よりコンポジションを優先する
  4. 既存コードに干渉しない設計を優先する
  5. 参照セマンティクスより値セマンティクスを優先する

各章のデザインパターンは、この5つをどう実現するかの具体例として読むと、全体像がつかみやすくなります。

第1章 ソフトウェア設計の技(ガイドライン1〜5)

設計とは「依存関係を管理する技」

第1章の主張は明快です。ソフトウェアは変更されるものであり、変更を難しくする最大の原因は依存関係です。

著者は、依存関係には次の2種類があると整理しています。

  • 問題そのものから生じる、必要な依存関係
  • 不注意や理解不足から生じる、人工的な依存関係

設計の仕事は、後者を減らし、必要な抽象化を導入することだ、という位置づけです。

開発の3レベル

ソフトウェア開発は、次の3つのレベルで捉えられています。

レベル 扱うもの
アーキテクチャ 全体にかかわる、後から変えにくい判断
設計 コンポーネント間の依存関係、デザインパターン
実装詳細 言語機能、メモリ、性能、実装パターン

境界は固定されたものではなく、流動的だと説明されています。

関心の分離とSRP

人工的な依存関係を減らす基本手段が関心の分離です。本書では SRP(単一責任の原則)を、「同じ理由で変更されるものをまとめ、そうでないものは分ける」ことと捉えています。

たとえば、次のように1つの基底クラスにエクスポートと永続化の責務を詰め込むと、実装ライブラリの選択がすべての派生クラスに波及します。

bad_report.cpp
class Report
{
 public:
   virtual ~Report() = default;
   virtual void exportToCsv() const = 0;   // CSVライブラリに依存
   virtual void persist(ByteStream&) const = 0;  // 保存形式に依存
};

変更理由の異なる2つの関心を、インタフェースごと分けるのが改善の方向です。

good_report.cpp
class CsvExportable
{
 public:
   virtual ~CsvExportable() = default;
   virtual void exportToCsv() const = 0;
};

class Persistable
{
 public:
   virtual ~Persistable() = default;
   virtual void persist(ByteStream&) const = 0;
};

DRYとYAGNI

SRP と並んで DRY 原則(同じ情報を複数箇所に書かない)が紹介されます。両者は多くの場合、互いに補強し合います。

一方で、著者は早すぎる抽象化にも釘を刺しています。将来の変更の種類が分かっていないのに分離を進めると、かえって生産性を損ないます。どんな変更が来るか見えるまで待ち、必要になってからリファクタリングする(YAGNI)という姿勢です。

ISP、テスト可用性、OCP

  • ISP(インタフェース分離の原則): SRP をインタフェースに適用した特殊ケースとして扱われます。テンプレート引数の要件を最小にするという考え方も、同じ原則の延長です。
  • テスト可用性: private メンバ関数をテストしたくなったら、friend や #define private public に頼るのではなく、その関数が置かれている場所が間違っていないかを疑うという提案が印象的でした。フリー関数や別クラスへ切り出すことで、カプセル化もテストしやすさも改善します。
  • OCP(開放/閉鎖原則): 拡張にはオープンに、改造にはクローズに。標準ライブラリの std::swap や std::hash が、カスタマイゼーションポイントを持つ設計の好例として挙げられています。

第2章 抽象化の技(ガイドライン6〜10)

期待される動作に従う(LSP)

第2章は、抽象化とは「要件や期待される動作を表現するもの」だという話から始まります。

ここで登場するのが、有名な Rectangle と Square の例です。幾何学的には正方形は長方形の一種ですが、setWidth と setHeight を個別に呼べるという基底クラスの期待を、Square は満たせません。

lsp_violation.cpp
void resize(Rectangle& r)
{
   r.setWidth(7);
   r.setHeight(4);
   assert(r.getArea() == 28);  // Squareを渡すと失敗する
}

つまり IS-A 関係を決めるのは現実世界の分類ではなく、インタフェースが約束する振る舞いです。これが LSP(リスコフの置換原則)の核心です。

基底クラスとコンセプトは同じ役割を持つ

動的多態(基底クラス)と静的多態(C++20 コンセプト)は、構文こそ違っても、どちらも「呼び出し側への要件を表現する」という点で本質的に同じだと説明されます。そのため、LSP や ISP はテンプレートにも当てはまります。

この章の後半では、関数オーバロードも抽象化の一種として扱われます。さらにアーキテクチャの観点から、DIP(依存関係逆転の原則) と、アーキテクチャを文書化することの意義が解説されます。

第3章 デザインパターンの目的(ガイドライン11〜14)

デザインパターンとは何かを整理する章です。著者はデザインパターンの性質を次の4つにまとめています。

  • 名前を持つ
  • 明確な目的を持つ
  • 抽象化によって依存関係を減らす
  • 長年検証されている

特に「名前を持つ」点の説明が分かりやすく、「ここは Visitor かな」「いや Strategy では」といった短い会話だけで、構造や拡張の方針まで共有できるというメリットが示されています。

また、よくある誤解として、デザインパターンは実装詳細ではないこと、そしてオブジェクト指向や継承階層に限った話ではないことが強調されます。C++ 標準ライブラリにもパターンが多用されており、「避けて通る方が難しい」という指摘も納得感があります。

第4章 Visitor パターン(ガイドライン15〜18)

最初に詳しく扱われるパターンが Visitor です。選ばれた理由は、「実装の選択肢が多く、選び方で結果が大きく変わる」ことを示すのに向いているからだと説明されています。

型を増やすか、処理を増やすか

ガイドライン15の問いが核心です。動的多態の設計では、「型の追加」と「処理の追加」のどちらを楽にしたいのかを先に決める必要がある、というものです。

拡張したいもの 向いている手法
型(新しい図形など) オブジェクト指向の継承階層
処理(新しい操作など) 手続き型、Visitor

古典的 Visitor と現代的 Visitor

  • 古典的な GoF の Visitor: 処理の追加は容易ですが、型の追加が難しく、循環依存などの短所もあります。
  • std::variant を使った現代的な実装: 多くの利点があり、閉じた型の集合に対する処理拡張に向いています。
  • Acyclic Visitor: 一見、古典的 Visitor の問題を解決するように見えますが、実行時オーバヘッドが大きいという落とし穴が解説されます。

第5章 Strategy パターンと Command パターン(ガイドライン19〜23)

Strategy は「処理方法を分離する」

Strategy は、動作を外から差し替え可能にするパターンです。著者は最も有用で重要なパターンの1つとして紹介しています。ポリシーベースの設計とも関係します。

strategy_sketch.cpp
// 描画方法を外部から注入する、という発想のスケッチ
class Circle
{
 public:
   using DrawStrategy = std::function<void(Circle const&)>;
   explicit Circle(double r, DrawStrategy d) : radius_(r), draw_(std::move(d)) {}
   void draw() const { draw_(*this); }
 private:
   double radius_;
   DrawStrategy draw_;
};

継承への向き合い方

ガイドライン20では、継承が不評を買いやすい理由が整理されます。ただし継承そのものが悪いわけではなく、多くのデザインパターンの威力は継承ではなくコンポジションから生まれる、というのが著者の見方です。

値セマンティクスへ

ガイドライン22〜23では、参照セマンティクスの落とし穴(ライフタイム管理や不正なポインタなど)を踏まえ、値セマンティクスへ移る流れが描かれます。std::function を使えば、Strategy と Command を値ベースで実装できます。

Command パターンについては、取り消し(undo)を含む、異なる種類の処理を抽象化する用途と、Strategy との違いが比較されます。

第6章 Adapter・Observer・CRTP(ガイドライン24〜27)

パターン 目的
Adapter 互換性のないインタフェースを、既存コードを変えずに橋渡しする
Observer 状態変化を観察し、通知を受け取る
CRTP 型同士の関係をコンパイル時に定義し、静的な抽象化を実現する

Adapter の例では、サードパーティ製クラスを既存の階層に組み込む場面が示されます。オブジェクトアダプタ、クラスアダプタ、関数アダプタといった種類の違いにも触れられています。

Observer は古典的な GoF スタイルに加え、現代的な C++ での実装も解説されます。CRTP は、基底クラスの正しい書き方に加え、コンパイル時の mixin クラスの作り方まで踏み込みます。

ガイドライン27では、抽象化を作るための文法的な継承と、技術的な便利さのための実装上の継承を区別して考える視点が提示されます。

第7章 Bridge・Prototype・External Polymorphism(ガイドライン28〜31)

この3つは、次章の Type Erasure の土台になるパターンです。

  • Bridge: 実装詳細からインタフェースを切り離し、物理的な依存関係(ヘッダのインクルード関係)を減らします。Pimpl イディオムはその最も簡潔な形です。
  • Bridge の性能面: ガイドライン29では、Bridge を使う場合、使わない場合、部分的に使う場合をベンチマークで比較しています。
  • Prototype: 仮想関数経由でコピーを作る仕組みです。コピー操作の抽象化を扱います。
  • External Polymorphism: 仮想関数の実装をクラスの外へ追い出し、関係をさらに希薄にします。

第8章 Type Erasure パターン(ガイドライン32〜34)

本書の山場の1つです。関心の分離と値セマンティクスという2本柱を組み合わせたパターンとして紹介されます。

歴史と位置づけ

Type Erasure は、継承階層を置き換えうる手法として語られます。Sean Parent 氏の講演をきっかけに広く知られるようになりましたが、技術自体はそれ以前から存在し、boost::function や std::any、std::function などにも使われています。

実装上のポイント

  • ガイドライン32: 所有権を持つ Type Erasure の基本実装
  • ガイドライン33: 性能に焦点を当てた例外的なガイドラインで、小さいバッファの最適化(SBO)や、仮想関数の手動ディスパッチを扱います。
  • ガイドライン34: 所有権を持たない(参照セマンティクスの)Type Erasure。値セマンティクスにつきまとうセットアップコストを踏まえた選択肢です。

第9章 Decorator パターン(ガイドライン35〜36)

Decorator は、既存コードに手を入れずに、オブジェクトへ責任(機能)を重ねていくパターンです。

導入の例では、価格の計算に税金や値引きなど複数の要因が絡む場面で、基底クラスにメンバ変数を足していくやり方や、継承階層を拡大していくやり方が破綻する様子が描かれます。クラス数が爆発し、再利用性も下がるためです。

その解として Decorator が示され、ガイドライン36では静的多態と動的多態の両方で、値ベースの現代的な実装が紹介されます。目的は同じでも見た目が大きく違うことが、デザインパターンの幅広さを実感させてくれます。

第10章 Singleton パターン(ガイドライン37〜38)

Singleton を扱う章ですが、主張は挑発的です。Singleton はデザインパターンではなく、実装パターンとして扱うべきというものです。第3章で挙げたデザインパターンの性質(特に、依存関係を減らす抽象化であること)を満たさない、というのがその理由です。

さらに、Meyers' Singleton の動作を解説したうえで、グローバルな状態が引き起こす問題(強い依存関係、変更やテストのしにくさ)にも正面から向き合います。そのうえで、適切に設計すれば、変更しやすさやテスト可用性と Singleton の恩恵は両立できる、という方向性が示されます。

第11章 最後のガイドライン(ガイドライン39)

最後のガイドラインは「デザインパターンの習得は継続すること」です。

本書で扱ったパターンが、どんな場面に向くかが一覧で振り返られます。

パターン 向いている場面
Visitor 閉じた型の集合に対する処理を拡張したいとき
Strategy 動作を外から変更可能にしたいとき
Command undo を含む、異なる処理を抽象化したいとき
Observer 状態変化を観察したいとき
Adapter 既存コードを変えずにインタフェースを変換したいとき
CRTP 仮想関数なしで静的な抽象化をしたいとき
Bridge 実装詳細を隠し、物理的依存を減らしたいとき
Prototype 仮想関数でコピーを作りたいとき
External Polymorphism 多態動作を外に構築して関係を希薄にしたいとき
Type Erasure 値セマンティクスと External Polymorphism を組み合わせたいとき
Decorator 既存コードに触れずに責任を追加したいとき

また、スマートポインタやファクトリ関数のような実装上のイディオムを、デザインパターンと混同しないように、という注意も添えられています。

読んで良かった点

1. 「言語機能に頼らない」という軸がぶれない

新機能を使えば設計が良くなる、という空気に対して、本書は終始冷静です。「悪い設計は言語機能では救えない」という主張が、章を追うごとに説得力を増していきます。

2. パターンを「目的」で語っている

実装の違いではなく、何のために依存関係を減らすのかという目的でパターンが整理されています。構造が似たパターン同士の違いが理解しやすくなりました。

3. 現代的な C++ との結びつき

std::variant、std::function、コンセプトなどを使い、古典的パターンを現代的に書き直す流れが丁寧です。GoF 本を読んだことがある人ほど、新鮮に感じるのではないでしょうか。

4. 「状況に依る」を隠さない

設計に唯一の正解はない、と繰り返し述べられます。万能なテンプレートを期待すると肩透かしですが、判断基準を身につけたい人には誠実な姿勢だと感じました。

気になった点・注意点

  • 初心者向けではありません。継承階層やテンプレートの経験がないと、議論についていくのが難しい場面があります。
  • 扱うデザインパターンの数は限られています。著者自身も、紙幅の都合でもっと取り上げたかったと述べています。
  • 一部のガイドラインは、実装の細部(Type Erasure のコストなど)にかなり踏み込みます。設計の全体像を先につかみたい場合は、第1〜3章と第11章から読むのも手です。

こんな人におすすめです

  • C++ の文法はひと通り分かるが、設計の考え方を体系的に学びたい方
  • SOLID 原則を知ってはいるものの、実コードでどう効くのかをつかみきれていない方
  • GoF のデザインパターンを、現代的な C++ で書き直す方法に関心がある方
  • 変更しにくいコードベースを前に、依存関係の整理方針を探している方

言語は C++ ですが、依存関係の管理や関心の分離という考え方は、他の言語での設計にも十分応用できると思います。

まとめ

本書の主張を一言でまとめると、**「設計とは依存関係を管理する技であり、デザインパターンはそのための検証済みの道具である」**ということになります。

  • 人工的な依存関係を減らす
  • 関心を分離し、変更理由ごとにまとめる
  • 継承よりコンポジション、参照より値を優先する
  • 将来の変更が見えないうちは、抽象化を急がない

この4つを頭に置いて、手元のコードを読み返してみると、新しい発見が必ずあるはずです。設計に悩む C++ エンジニアには、手元に置いておきたい1冊だと感じました。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?