優れたAPIの特徴
結論からいうと,
- 内部実装が隠蔽されていて,
- 使い方がわかりやすく,
- 疎結合であること
問題が抽象化されていること
APIは解決すべき問題の抽象概念を提供すべき. ハイレベルな概念に基づいてデザインが決定されるべきであり, 内部実装の課題を開示すべきではない.
ここでいう抽象化された問題とは, 「名前と住所を管理する」といったことであり, APIで提供されるクラスと関数はその問題を解くためにあるということが表現されているべきであるということ. Personオブジェクトのリストで管理するなどという内部実装の内容はクライアントには知られてはならない.
実装の隠蔽
本書で繰り返し語られる超重要なテーマ. 実装が隠蔽されることによって, クライアントは内部実装に依存したコードを書けなくなり, 後でクライアントコードに影響を与えずにAPIの実装を変更できるようになる. クライアントにハックされないAPIを構築すること.
- .hファイルに宣言を, .cppファイルに実装を書くこと. 原則的に, ヘッダには実装を書いてはならない.
- クラスのメンバ変数はprivateにすること. publicにするとクライアントコードで予期せぬ使われ方がされ, 後の変更が不可能になる. privateにしてgetter/setterを用意し, その実装をcppファイルの中に隠すことで, 通知, 排他制御, 遅延評価, キャッシュの利用など, 後の変更が用意になる. getterのみ用意してイミュータブルオブジェクトにすることもできる. メンバ変数はprotectedにしてもいけない. クライアントがサブクラスを作成すればメンバ変数を自由に操作できてしまうからである.
- パブリックにする必要のないメンバ関数も隠蔽すべき. クラスというものは, 何をするかを定義すべきであり, どのようにするかを定義するのではない. privateメンバ関数にするのでも不十分である. 公開されたヘッダは書き換え可能だからだ. private関数は.cppファイルに移動してstatic関数に変換し, ヘッダから削除すること.
- 特に, privateメンバ変数を非constのポインタor参照で返すメンバ関数は内部状態のリークであり, クラスの管理なしにクライアントによって変更されうるので, 絶対に避けなければいけない.
- 可能な限りPimplイディオムを採用すること. これにより, クラスの内部実装を完全に.cppファイルに隠蔽することができる.
- private関数と同様の理由により, 実装の詳細のみに必要なクラスも, すべて.cppに隠蔽すべき.
例: httpサーバーからファイルダウンロード
#include <string>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
// 悪い例. クライアントが知らなくて良いメソッドが多すぎ
class UrlDownloader {
public:
UrlDownloader();
bool DownloadToFile(const std::string& url, const std::string& localfile);
bool SocketConnect(const std::string& host, int port);
void SocketDisconnect();
void IsSocketDisconnect() const;
int GetSocket() const; // 内部状態のリーク. クライアントが自由にソケットを直に操作できる
bool SocketWrite(const char* buffer, size_t bytes);
size_t SocketRead(char* buffer, size_t bytes);
bool WriteBufferToFile(char* buffer, const std::string& filename);
private:
int socket_id_;
struct sockaddr_in serv_addr_;
bool is_connected_;
};
#include <string>
#include <memory>
// 良い例. ユーザーシナリオ上必要なメソッドだけ提供
class UrlDownloader {
public:
UrlDownloader();
~UrlDownloader();
// 書籍ではファイル名を指定しているが, 依存性の注入の観点から, std::ostreamを渡すなどとしたほうが良い
bool DownloadToFile(const std::string& url, const std::string& localfile);
private:
// Pimplイディオムで実装を隠蔽
struct Impl;
std::unique_ptr<Impl> impl_;
};