優れたAPIの特徴(再掲)
- 内部実装が隠蔽されていて,
- 使い方がわかりやすく,
- 疎結合であること
優れたAPIは最小限に完全であるべき
APIに期待されている機能が十分に提供されていて, 必要以上の機能は提供されていないことが重要.
約束し過ぎないこと
つまり, 機能を追加し過ぎないこと. 一旦APIをリリースしてしまうと, 既存機能を削除するのは非常に難しい. 明白に必要なものだけ提供し, 不確かなものは書かないでおくこと.
仮想関数の追加は慎重に
仮想関数の宣言は有効かつやむを得ない必要性が無い限り, 避けること. オーバーライドによってAPIの完全性が失われる可能性があるからだ. サブクラス化も, 意味があるとき(is-a関係)のみ許可すべきだ.
テンプレートメソッドパターンの使用も合わせて検討すること. 公開インターフェースを非仮想にすることで, 結合度が下がる.どうしても仮想関数をパブリックに使う場合, 以下の原則に従うこと:
- ベースクラスに仮想デストラクタを宣言すること.
- クラスのメソッド間の関連をドキュメント化すること.
- コンストラクタまたはデストラクタから仮想関数を呼び出さないこと.
コンビニエンスAPI
コアAPIを最小限に保とうとすると, クライアントが簡単に使えなくなるかもしれない. そんな時はコアAPIのパブリック関数をラップする補助APIを提供することを検討しよう. その場合, 補助APIはコアAPIと完全に独立しているようにすること. こうすることで, APIを使いやすくしつつ, 最小設計を維持することができる.
コアAPIクラスのメソッドを無秩序に増やさず, 基本機能と高水準の機能を別のレイヤーに構築すること.
使いやすさ
ヘッダの宣言を見ただけで使い方がわかるようになっていること. 気を散らす斬新なインターフェースではなく, 既存のモデルやパターンを活用し, ユーザーを目の前のタスクに集中させること.
解明しやすさ
付属の説明やドキュメントを読まなくても, 自分で使用法を解明できるAPIであること. そのためには,
- 直感的で論理的なオブジェクトモデルを採用すること
- クラスや関数にわかりやすく適切な名前をつけること(略語は使わないように!)
間違いの防止
使いやすいだけでなく, 誤用しにくいAPIであること.
例えば, フラグはbool型ではなくenum型を採用することで, 間違いをコンパイルエラーにできる.
// 良くないインターフェース
std::string FindString(const std::string& text, bool search_forward, bool case_sensitive);
// 良いインターフェース
enum class SearchDirection { FOWARD, BACKWARD };
enum class CaseSensitivity { CASE_SENSITIVE, CASE_INSENSITIVE };
std::string FindString(const std::string& text, SearchDirection direction, CaseSensitivity case_sensitivity);
さらに踏み込んだ日付型の例が本書では掲載されている.
一貫性
提供するAPI全体を通して, 一貫したデザインポリシーを持って, 使い方のルールを覚えやすく, 採用しやすくすべきである.
- 一貫した命名. 同じ概念には同じ名前を使うこと. 例えば, begin() に対して end() を対応付けたら, start(), finish() は混ぜてはいけない. prev を使ったり previous を使ったりしてはならない. 略語はなんとしても避けるべきである.
- 引数の順序にも一貫性を持たせること. 出力の引数が最初に出現したり, 最後に出現したりと, バラバラなのはお粗末なAPIの例である.
- 似た役割を持つクラスは, 一貫して似たインターフェースを提供すべき. 例として, STL のコンテナは begin(), end(), size() など, 名前を同じくするメソッドを提供している. 各種コンテナを使う際に, 常に慣れた方法でプログラミングすることができる.
- というわけで API を記述する際は STL のパターンを真似るのがベターである.
相互独立性
解釈が2つある.
副作用のない関数
メソッド呼び出しが副作用をもたらさないようにすべき. ある特定の状態を変更する API は, その他のどの状態をも変更してはならない. そのようにすることで, API の動作が予測可能になり, 変更の際の影響も局所化できる.
相互独立性を持ったAPIを設計する上で,
- 冗長性の削減: 1つの情報を2つ以上の方法で表してはいけない. アクセスメソッドを信頼できる1つに限定すること.
- 独立性の拡大: 開示する概念に重複する意味があってはならない. コンポーネントと概念は1対1対応するべき.
データとアルゴリズムの分離
すべての異なるオペレーションは, それぞれの使用可能なデータ型に適用できるのが良い. 例えば STL では, std::count は std::vector, std::map, std::set などのどのコンテナにも適用できる.
リソース管理
スマートポインタを使って, API を使いやすくしよう. メモリ管理でクライアントを煩わせないこと. C++でのよくあるメモリ関連エラーは std::shared_ptr, std::weak_ptr, std::unique_ptr を使うことでこの種の問題の大半が回避できる. new, delete を生で書いたら負け.
- Nullポインタの間接参照 -> std::weak_ptr で検出可能
- 2重開放 -> そもそも delete を書かなくて OK
- ダングリングポインタ -> std::weak_ptr で検出可能
- アロケータの混同(mallocしてdeleteなど) -> カスタムデリータで回避
- 配列の不正な開放(delete[]のところをdelete] -> カスタムデリータで回避
- メモリリーク -> std::shared_ptr, std::unique_ptr で回避
ファクトリ関数など, ポインタを返す関数がある場合,
- クライアント側に開放する責任がある -> std::shared_ptr / std::unique_ptr で返すべき.
- API側に所有権がある(プールなど) -> 標準のポインタで返すことができる. クライアント側でdeleteしないように明示しておくこと.
このようなリソース管理イディオムは排他制御, ファイルハンドル, ソケットなどでも有効である(RAII). 何らかのリソース割り当てと解除の機構を提供する場合, それを管理するクラスを提供して, コンストラクタで割り当て, デストラクタで開放するようにするべきである. その際, デストラクタで例外を投げないように注意すること.
プラットフォーム独立性
パブリックヘッダにプラットフォーム固有の ifdef を入れてはいけない. 実装の詳細が漏れているからだ. ifdef で囲まれた関数を使う際には, クライアントコードも ifdef を書かなければならなくなる. さもないとクライアントコードはコンパイルできない. プラットフォームのアップデートにも弱い.
プラットフォーム差異は実装ファイルに隠蔽し, すべてのプラットフォームで一貫した API を提供すべきだ.
// ダメな例. 位置情報取得APIにプラットフォーム依存性がある
class MobilePhone {
public:
bool StartCall(const std::string& number);
bool EndCall();
#ifdef TARGET_OS_IPHONE
bool GetGPSLocation(double* let, double* lon);
#endif
};
// 良い例
class MobilePhone {
public:
bool StartCall(const std::string& number);
bool EndCall();
bool HasGPS() const;
bool GetGPSLocation(double* let, double* lon);
};
// 以下 cpp ファイルで定義
bool MobilePhone::HasGPS() const
{
#ifdef TARGET_OS_IPHONE
return true;
#else
return false;
#endif
}
...