C++のためのAPIデザイン 読書メモ 第2章 その2
優れた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() など, 名前を同じくするメソッドを提供している. 各種コンテナを使う際に, 常に慣れた方法でプログラミングすることができる.
相互独立性
解釈が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 } ...
- 作者: マーティン・レディ
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2013/11/15
- メディア: Kindle版
- この商品を含むブログを見る