DbCとは?
最近達人プログラマーを読み返しているのですが、気になる概念があったのでこの機会にまとめておきます。
そう、DbCという設計手法です。
Design by Contract の略で、日本語では"契約による設計"といいます。
Eiffel というオブジェクト指向プログラミング言語を開発したBertrand Mayer氏によって提唱された、プログラムの正しさを保証するための設計手法です。
基本的な考え方
ソフトウェア部品や関数の使い方・責任範囲を"契約"のように明示して設計する手法です。
"契約"と同じように、使う側、使われる側双方に義務と権利が生じるわけです。
例えば、売買の契約においては、買い手を売り手で以下のような権利と義務が生じます
| 誰が | 義務 | 権利 |
|---|---|---|
| 買い手 | 代金を支払う | 商品を受け取る |
| 売り手 | 商品を引き渡す | 代金を受け取る |
仮に買い手側が代金を支払うことを放棄したり、支払った代金が商品の価格より低かった場合、売買契約は成立しませんね。売り手側が代金に応じた商品を引き渡せなかった場合も同様です。
このような性質をプログラミングに応用したのがDbCです。
3つの要素
では、具体的に誰がどのような義務と権利を請け負うべきなのか?
3つの要素に分けて定義されています。
事前条件
メソッドを呼び出す前に満足させておかなければならない条件のことです。
事前条件を満たしていなければ、メソッドは呼び出されるべきではありません。
つまり、事前条件を満たすのは呼び出し元の義務です。
引数に想定外の値渡したときは例外出したりアプリがクラッシュしたりするけどそれは呼び出し元で管理してねってことです。ある機能を呼び出す場合は呼び出し元で事前条件を満たしてから呼び出しましょう。
事後条件
メソッドが実行された後に保証される条件のことです。
事前条件が満たされた上で機能が実行された場合、無限ループで処理が終わらなかったり、メソッドが返すべき値を返すことができていなかったりするとプログラムが破綻してしまいます。
事後条件を満たすべきなのは呼び出し先(メソッド側)の義務です。
事前条件を満たした上でメソッドを呼んだのであれば、その呼び出し先の実装がどうなっていようが呼び出し元では期待値が返ってくることが保証されているということです。
クラス不変表明
クラスが呼び出し元に対して常に維持すべき状態を守ることです。
メソッドの呼び出し後にクラスが外部に対して維持すべき状態を壊してはいけません。
これも呼び出し先の義務となります。
例えば、銀行口座を管理するようなクラスで"残高をマイナスにしてはいけない"というルールがあるとします。呼び出し側が指定した額で"引き出し"というメソッドを呼び出したとき、処理完了後に残高がマイナスになるような変更を加えてはいけません。
残高を超えた金額を引き出そうとしている場合、呼び出されたメソッド側でどのように処理を止めるが考える必要があります。
ここまでのまとめ
ここまでをまとめると、呼び出される機能と呼び出し側が結ぶ契約は以下となります。(達人プログラマーより引用)
呼び出し側によって、呼び出される機能の事前条件がすべて満足された場合、当該機能は処理完了時点ですべての事後条件と不変表明を満足させるものとする。
いずれかが契約条件を履行できなかった場合は、例外のスローやプログラムの終了といった結果が引き起こされるべきです。
C#ではどう実装する?
以前までは.NET標準のコードコントラクトを使って実装されていたみたいですね。ただ、.NET5以降ではサポートが終了しており、現在は推奨されていないようです…。
今後触ることはなさそうなのでコードコントラクトを使った実装をやってみました。
using System.Diagnostics.Contracts;
public class BankAccount
{
private int balance;
// 口座から引き出す
public void Withdraw(int amount)
{
// 事前条件
Contract.Requires(amount > 0);
Contract.Requires(amount <= this.balance);
// 事後条件
Contract.Ensures(balance == Contract.OldValue(this.balance) - amount);
// 実処理
this.balance -= amount;
}
// クラス不変条件を定義するメソッド
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(balance >= 0);
}
}
事前条件など契約違反があった場合、ContractExceptionをThrowされます。
代替策
では、現在はどのようなアプローチが推奨されるのか?
- 静的解析ツール(Roslyn)を使って契約違反を検出
- ユニットテストで検出
- ガード節で検出
事前条件のチェックだけであれば3のガード節で今すぐにでも実践できますね!
↓ こんな感じで
// 口座から引き出す
public void Withdraw(int amount)
{
// 事前条件であればガード節で検出できる
if (amount > 0 || amount <= this.balance)
{
throw new ArgumentException();
}
this.balance -= amount;
}
参考