C++のためのAPIデザイン 読書メモ 第2章 その3
優れたAPIの特徴(再掲)
- 内部実装が隠蔽されていて,
- 使い方がわかりやすく,
- 疎結合であること
疎結合
優れたAPIは結合度が低く凝集度が高い. コンポーネント間の結合を可能な限り低く保とう. 疎結合であるとは,
- クラスのメソッドの数, メソッドあたりの引数が少なければ, それを呼び出すコンポーネントとの結合度は低い.
- メソッド内部でグローバル変数を書き換えるのは, 結合度が上がる.
- クラスの継承は, クラスの合成(コンポジション)よりも結合度が高い. サブクラスがベースクラスのprotectedメンバにアクセスできるから.
- メソッドのシグネチャを変えた時に, このメソッドに依存するすべてのコードの変更が局所的に行えるならば, 結合度は低い.
名前だけの結合
可能な限り前方宣言を使おう. ヘッダで別のヘッダをインクルードすると, 依存性が伝搬していく.
良くない例:
#include "my_object.h" class MyObjectHolder { public: MyObjectHolder(const MyObject& my_object); const MyObject& GetMyObject() const; private: MyObject my_object_; };
良い例:
class MyObject; // 前方宣言 class MyObjectHolder { public: MyObjectHolder(const MyObject& my_object); const MyObject& GetMyObject() const; private: MyObject* my_object_; };
メンバ関数よりフリー関数を使おう
フリー関数はクラスのパブリックメンバにしかアクセス出来ないので, 結合度が下がる. また, クラスの機能を最小限に保つことができる.
良くない例:
class Foo { public: ... const std::string& GetName() const; void PrintName() const; // 便利メソッドはクラスから独立させるべき ... private: std::string name_; };
良い例:
class Foo { public: ... const std::string& GetName() const; ... private: std::string name_; }; void PrintName(const Foo& foo);
意図的な冗長性が時には正当化される
論理的にはコンポーネントAがコンポーネントBに依存しているが, ごく一部分にしか依存していない時, 依存部分だけを抽出し, Aにも同じ情報を持たせることで, A→Bの依存を断ち切ることができる.
// b.h class B { public: // たくさんのメンバ関数 private: std::string name_; // その他たくさんのメンバ変数 }; // a.h #include "b.h" class A { public: ... private: B b_; };
上記は, AがBの名前のみ必要である場合に, 以下のようにできる.
class B { public: // たくさんのメンバ関数 private: std::string name_; // その他たくさんのメンバ変数 }; // a.h class A { public: ... private: std::string name_; };
マネージャクラスでローレベルクラスをカプセル化
個人的な経験ではXxxManagerクラスは地雷である場合が多く, 特に推奨したくないのでスキップ.
コールバック, オブザーバー, 通知
イベント発生時に他のクラスに通知する場合の手法.
コールバック
モジュールBにモジュールAの関数を渡し, 必要なときにBがAの関数を呼び出す仕組み. C++11以前なら関数ポインタ, 以降ならstd::function.
class canceled : public std::runtime_error {}; class B { public: B(std::function<void()> callback); void DoSomething(); ... private: std::function<void()> callback_; ... }; B::B(std::function<void()> callback) : callback_(callback) {} void B::DoSomething() { callback_(); // do something... } // 状態を出力したり B([]{ std::cout << "Do something." << std::endl; }).DoSomething(); // キャンセルチェックしたり std::atomic<bool> has_canceled; B([&has_canceled]{ if (has_canceled) { throw std::runtime_error("canceled"); } }).DoSomething();
オブザーバーパターンは第3章で詳しく説明.
通知
互いに関連しないコンポーネント間で通知を送る仕組みに関して. シグナル/スロットがよく使われる.
// 引数なし戻り値なしのシグナルを作成 boost::signal<void()> signal; // シグナルにスロットを接続 signal.connect([]{ std::cout << "MySlot called." << std::endl; }); // シグナルを発行することで, 接続されたスロットが呼ばれる signal();
安定性, 文書化, テスト
- 適切にバージョン管理されて後方互換性が保たれていること.
- 十分に文書化されていれば, ユーザーがエラー条件やベストプラクティスなどについての明確な情報が得られる.
- 自動テストがあれば既存のユースケースを壊さずにAPIを変更できる.
- 作者: マーティン・レディ
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2013/11/15
- メディア: Kindle版
- この商品を含むブログを見る