LoginSignup
6
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-04-12

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

6
6
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
6
6