優れた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を変更できる.