契約プログラミング
1. 目的
2. 課題
過剰な防御的プログラミング
- 「もし引数がNULLだったら」「もし空文字だったら」というチェックが各所に散らばり、本質的なロジックが埋もれてしまう
- 本質的なロジックが埋もれてしまい、処理を理解するコストが跳ね上がる
責任の曖昧さ
- バグが発生した際、値を渡した側(呼び出し側)が悪いのか、値を処理した側(関数側)が悪いのかの判断に時間がかかる
- 結果的に呼び出し元/関数側双方で過剰な防衛的プログラミングが行われる
ドキュメントの形骸化
- 仕様書に「引数は正の数のみ」と書いてあっても、コード自体がそれを強制しないため、仕様と実装が乖離する
3. 解決策
アサーションの追加
- プログラム内に「実行可能な契約(=アサーション)」を埋め込む
- アサーションは事前条件、事後条件、不変条件を検証する
- 事前条件 (Precondition):
- 呼び出し側が守るべきルール(例:引数の範囲チェック)
- 事後条件 (Postcondition):
- 不変条件 (Invariant):
- これらの契約をコードとして記述し、条件を満たさない場合は即座にエラー(例外)を発生させることで、不正な状態での実行を阻止する
4. メリット
デバッグの高速化
- 契約に違反した瞬間にエラーが出るため、バグの混入箇所を特定しやすい
コードの簡素化
- 「事前条件を呼び出し側が守る」という前提があるため、関数内部での重複したエラーチェックを削減できる
自己文書化
- コードそのものが仕様を表すため、正確な使い方が開発者に伝わりやすい
5. デメリット
実行負荷の増加
- 実行時に常に条件チェックを行うため、パフォーマンスに影響を与える
- ただし、現代のハードウェア環境では無視できる程度
記述コストの増大
- 本来のロジック以外に契約条件を書く手間がかかり、コード量が増える
- ただし、場当たり的な防衛的プログラミングよりは整理される分マシではある
言語サポートの限定
- 多くの主要言語(C#, Java, Python等)では標準機能として不十分であり、ライブラリやアサーション機能を工夫して使う必要がある
- 主要な言語ではアサーションを言語機能として持たず、ユニットテストで代替することが多い
6. 注意事項
例外処理との混同を避ける
- 契約違反は「プログラムのバグ(修正すべきもの)」である
- 一方で、ファイルが見つからない、ネットワークが切れるといった「外部要因の異常」は、契約ではなく通常の例外処理(Try-Catchなど)で扱うべきである
本番環境での運用
- パフォーマンスを優先し、本番環境では契約チェックを無効化することが一般的
- そのため、契約チェックに依存してビジネスロジック(データのバリデーションなど)を実装してはならない
継承のルール(リスコフの置換原則)
- 子クラスで契約を書き換える場合、「事前条件を強めること」や「事後条件を弱めること」は許されない
- これはオブジェクト指向の整合性を壊す原因となる
参考資料
Appendix-1 契約プログラミング概要
契約プログラミングとは
- コードが仕様通りの条件(契約)を満たすことを要求するプログラミング技法
条件とは
事前条件
- サブルーチン開始時に、呼び出し側が保証すべき条件(状態)
事後条件
- サブルーチン終了時にサブルーチンが保証すべき条件(状態)
不変条件
- クラスなどのオブジェクトが外部に公開している操作の開始/終了時に保証されているべき条件(状態)
事後条件について
- メソッドが終了時に保証されるべき条件
- 逆に言えば、あってはならない条件(状態)をチェックする
- ex.
- 結果がマイナスになっていないこと
- データ構造が仕様通りであること
- ビジネスルール上、異常な状態になっていないこと(例えば、未成年にアダルト商品が販売できてしまう、など)
使い処
- 全てのメソッドに契約を定義するのは処理が煩雑になる上、保守性にも悪影響を与える
- 事前条件は入力値バリデーション処理に一任し、以降の処理はバリデーション済みのデータを信用するという考え方はバランスが良い
- 事後条件は複雑な組み合わせチェックが必要なドメインモデルのセルフチェック処理として実装するのが良いかもしれない