本ドキュメントの目的
- 契約プログラミングに基づくAssertの適切な活用方針を示す。
はじめに
プログラミングのバグを防ぐための方法として、契約プログラミング(Design By Contract)という概念がある。
プログラミング言語によっては、契約プログラミングをサポートしているものもあるが、本ドキュメントでは、多くの言語に存在するであろうAssertを活用した契約プログラミングに基づくコーディング指針を示す。
Assertの活用によるバグの減少はMicrosoft社の論文にも記載されている。
契約プログラミング(Design By Contract)について
契約プログラミングとは、変数などがプログラム内のあるタイミングで満たされるべき状態を明示し、それが満たされるかどうかをプログラム内で評価するという考え方である。
状態を満たすべきタイミングによって、以下の3つの条件が存在する。
- 事前条件
- 事後条件
- 不変条件
以下、これら3つの条件について述べる。
事前条件
ある処理(関数)が実行される前に満たすべき条件。
例えば、除算をする関数の引数は0ではないこと、2乗根される変数の値は正の数であることなどが挙げられる。
事後条件
ある処理(関数)が実行された後に満たすべき条件。
例えば、百分率の計算結果は100を超えないことなどが挙げられる。
不変条件
いついかなるタイミングにおいても、あるデータ(変数など)が常に満たしているべき条件。
例えば、自然数を格納する変数の値は0より大きい、日付を格納する変数xの値は1 <= x <= 31
であることなどが挙げられる。
Assertを活用した実装例
本項目では、Assertを使用することのメリットについて述べた後、Assertによる実装が容易な事前条件と事後条件の2つの条件についてのユースケースと実装例を示す。
なお、不変条件については言語機能によるサポートが必要な場合があるため、本項では割愛する。
Assert活用のメリット
Assertを活用することで、処理(関数)の仕様を条件として、プログラム内に記述することが可能である。また、仕様に反した処理の呼び出しを検知することも可能である。
e.g.1 2乗根を求める関数
引数xの2乗根を返す関数calc_sqrt(x)
の実装を考える。
以下のような事前条件と事後条件が考えられる。
事前条件
xの値は0以上である。
事後条件
2乗根を計算した結果ans
の値が0以上である。
これらの条件を基にしたpythonでの実装例は以下の通りである。
import math
def calc_sqrt(x):
assert x >= 0 # 事前条件
ans = math.sqrt(x)
assert ans >= 0 # 事後条件
return ans
def main():
num = calc_sqrt(4) # 事前条件が成立している。
print(num)
num = calc_sqrt(-4) # 事前条件が成立していないためプログラムはここで終了。
print(num)
if __name__ == '__main__':
main()
e.g.2 プリペイドカードの残高管理ロジック
pythonでの実装例を先に示す。
なお、このコードではあるべきエラーハンドリングが実装されていない。次項で実装する。
class prepaid_card:
'''
precharge:元残高
amount:現在の残高
price:支払い価格
'''
def __init__(self, precharge):
assert precharge >= 0 # 事前条件
self.amount = precharge
assert self.amount >= 0 # 事後条件
def pay(self, price):
assert price >= 0 # 事前条件
self.amount -= price
print("price:{}Yen,amount{}Yen".format(price, self.amount))
assert self.amount >= 0 # 事後条件
def main():
prepaid = prepaid_card(1000)
prepaid.pay(300)
prepaid.pay(500)
prepaid.pay(-200)
if __name__ == '__main__':
main()
本例では、プリペイドカードの元残高と支払い価格が正の数であることを事前条件とし、プリペイドカード登録時と支払い処理後の現在の残高が正の数であることを事後条件としている。
これにより、不正な値の受け渡しと支払い処理のバグを防ぐことができた。しかし、現在の残高を超える支払い価格が渡された場合の処理が未実装である。
#例外処理によるエラーハンドリングをすべき場合
以下のようなケースでは、Assertは用いずに例外処理でエラーハンドリングをすべきである。
- ユーザによる入力や設定ファイル等による、外部入力で不正なデータが入力された場合
- ファイルアクセスやネットワーク等のエラーが発生した場合
- ビジネスロジック上のエラーである場合
e.g.1にて、xの値を標準入力で受け付ける場合は、例外処理を用いて以下のような実装となる。
import math
from sys import stdin
class InputValueError(Exception):
'''
不正な値が入力された場合の例外クラス
'''
pass
def std_input():
'''
標準入力受付関数
'''
input_num = float(stdin.readline().rstrip())
return input_num
def calc_sqrt(x):
assert x >= 0 # 事前条件
ans = math.sqrt(x)
assert ans >= 0 # 事後条件
return ans
def main():
input_num = std_input()
try:
if input_num < 0: # 不正な値が入力された場合は例外処理を行う
raise InputValueError("正の数を入力してください。")
num = calc_sqrt(input_num)
print(num)
except InputValueError as e:
print(e)
if __name__ == '__main__':
main()
e.g.2において、現在の残高を超える支払い価格が渡された場合という状況はビジネスロジック上のエラーである。すなわち、例外処理で対応すべきである。これを考慮した実装例は以下のようになる。
class AmountValueError(Exception):
'''
残高が足りない場合の例外クラス
'''
pass
class prepaid_card:
'''
precharge:元残高
amount:現在の残高
price:支払い価格
'''
def __init__(self, precharge):
assert precharge >= 0 # 事前条件
self.amount = precharge
assert self.amount >= 0 # 事後条件
def pay(self, price):
assert price >= 0 # 事前条件
try:
if self.amount < price: # 残高が足りない場合は例外処理
raise AmountValueError("残高が足りません")
self.amount -= price
print("price:{}Yen,amount{}Yen".format(price, self.amount))
except AmountValueError as e:
print(e)
assert self.amount >= 0 # 事後条件
def main():
prepaid = prepaid_card(1000)
prepaid.pay(300)
prepaid.pay(500)
prepaid.pay(300) # この処理は残高が足りないので、例外処理される。
prepaid.pay(100)
prepaid.pay(-200)
if __name__ == '__main__':
main()