この文書はCode Health: Make Interfaces Hard to Misuseの翻訳です。
自分のコードの中のエラーは常に避けるようにするのは当然だが、君のコードの呼び出し側が作り出すエラーについてはどうだろう?いいインタフェースデザインは呼び出し側が正しく動作するのを容易にし、間違ったことをするのを難しくする。君のクラスに求められる不変性の維持という責任を呼び出し側に押し付けないようにしよう。
このコードが引き起こしうる問題が分かるだろうか?
class Vector {
explicit Vector(int num_slots); // `num_slots`個のスロットを持つ空のvectorを作る
int RemainingSlots() const; // 現在残っているスロットの数を返す
void AddSlots(int num_slots); // vectorにさらに`num_slots`個、追加する
// 新たな要素をvectorの最後に追加する。呼び出し側はこのメソッドを呼び出す前に
// RemainingSlots()が少なくとも1を返すことを気をつけること。そうでない場合は、
// 呼び出し側はAddSlots()を呼ばなくてはならない。
void Insert(int value);
}
もし、呼び出し側がAddSlots()
を呼び出すことを忘れると、Insert()
が呼ばれたときに未定義動作が起きるかもしれない。 このインタフェースは複雜さを呼び出し側に押し付け、実装の詳細をさらしている。
スロットを管理することは呼び出し側に見せるクラスの挙動としては適切ではなく、インタフェースとして外に出さないようにしよう。そうすることで、Insert()
の際に必要となるスロットの追加に関する未定義動作を引き起こさないようにできる。
class Vector {
explicit Vector(int num_slots);
// 新たな要素をvectorの最後に追加する。必要に応じて、
// 新たな値に対する保管場所として新たなスロットが割り当てられる。
void Insert(int value);
}
コンパイラーによって規定される契約の方が実行時のチェックによって規定される契約よりも良い。さらに悪いのは、文書だけによって規定される契約だ。それは呼び出し側が正しいことをするかどうかにかかっている。
インタフェースが間違って使われやすいシグナルとなる他の例を挙げる:
-
呼び出し側に初期化関数の呼び出しを要求する(代案:ファクトリーメソッドを公開し完全に初期化されたオブジェクトを返す)
-
呼び出し側に特別なクリーンナップの実行を要求する(代案:オブジェクトがスコープから外れたら自動的にクリーンナップされるような言語固有の構成の仕方を使う)
-
必要なパラメータなしでオブジェクトを作成できる方法がコードにある(例えば、IDなしのユーザー)
-
いくつかの値だけが有効なパラメータがある。特に、より適切な型を使うことができるにも関わらず。(例えば、
int timeout_in_mills
よりもDuration timeout
の方がいい)
現実的に、常に誰でも間違いなく使えるインタフェースとできるわけではない。特定のケースでは、静的解析やドキュメントに頼ることもある。 というのも、いくつかの要求はインタフェースとして表現することは不可能である。(例えば、コールバック関数がスレッド安全でなければならないなど)
強制しなくていいことを強制するのはやめよう。コードはガチガチにならないように。例えば、関数のパラメータの広範な検証は複雜性を増し、パフォーマンスを低下させる。