atoshのブログ

一生 C++ します!

C++のためのAPIデザイン 読書メモ 第2章 その3

優れたAPIの特徴(再掲)

  • 内部実装が隠蔽されていて,
  • 使い方がわかりやすく,
  • 疎結合であること

疎結合

優れたAPIは結合度が低く凝集度が高い. コンポーネント間の結合を可能な限り低く保とう. 疎結合であるとは,

名前だけの結合

可能な限り前方宣言を使おう. ヘッダで別のヘッダをインクルードすると, 依存性が伝搬していく.

良くない例:

#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を変更できる.

C++のためのAPIデザイン

C++のためのAPIデザイン