契約プログラミング
契約プログラミングは、ソフトウェアの一部を、互いにサービスを提供する個別のエンティティとして扱うソフトウェア設計手法だ。この手法では、サービスの提供者と利用者が契約に従っている限り、ソフトウェアは仕様どおりに動作することが実現される。
Dの契約プログラミング機能では、ソフトウェアサービスの単位として関数が使用される。ユニットテストと同様に、契約プログラミングもassert
チェックに基づいている。
Dの契約プログラミングは、3種類のコードブロックによって実装されている。
- 関数
in
ブロック - 関数
out
ブロック - 構造体およびクラス
invariant
ブロック
invariant
ブロックと契約継承については、構造体とクラスについて説明した後、後の章で説明する。
in
事前条件用のブロック
関数の正しい実行は、通常、そのパラメータの値が有効であるかどうかによって決まる。例えば、平方根関数は、そのパラメータが負ではないことを要求する場合がある。日付を扱う関数は、月の番号が1から12までの間であることを要求する場合がある。このような関数の要件は、その関数の前提条件と呼ばれる。
このような条件チェックは、assert
およびenforce
の章で既に紹介した。パラメータ値の条件は、関数定義内のassert
チェックによって強制することができる。
契約プログラミングでは、同じチェックが関数のin
ブロック内に記述される。in
またはout
ブロックを使用する場合、関数の実際の本体はdo
ブロックとして指定する必要がある。
注釈:以前のバージョンのDでは、do
の代わりにbody
キーワードがこの目的で使用されていた。
in
ブロックの利点は、すべての前提条件をまとめて、関数の実際の本体から分離できることだ。こうすることで、関数本体には前提条件に関するassert
チェックが不要になる。必要に応じて、関数本体内に、関数本体内の潜在的なプログラミングエラーを防ぐための、関連のないチェックとして、他のassert
チェックを挿入することは、依然として可能であり、お勧めだ。
in
ブロック内のコードは、関数が呼び出されるたびに自動的に実行される。関数の実際の実行は、in
ブロック内のすべてのassert
チェックが合格した場合にのみ開始される。これにより、無効な前提条件で関数が実行されるのを防ぎ、その結果、誤った結果が生成されるのを回避できる。
in
ブロック内でassert
チェックが失敗した場合は、呼び出し元によって契約が違反されたことを示している。
out
後置条件用のブロック
契約のもう一方は、関数が提供する保証である。このような保証は、関数の事後条件と呼ばれる。事後条件を持つ関数の例としては、2月の日数を返す関数がある。この関数は、返される値が常に28または29であることを保証できる。
後条件は、関数のout
ブロック内でチェックされる。
return
文によって関数が返す値は、関数内で変数として定義する必要がないため、通常、戻り値を参照する名前はない。これは、assert
がout
ブロック内でチェックする際に、返された変数を名前で参照できないため、問題となる場合がある。
Dは、out
キーワードの直後に戻り値の名前を指定する方法を提供することで、この問題を解決している。この名前は、関数が返している値そのものを表す。
result
は返される値として妥当な名前だが、他の有効な名前も使用できる。
一部の関数には戻り値がないか、戻り値をチェックする必要がない。その場合、out
ブロックは名前を指定しない。
in
ブロックと同様に、out
ブロックは、関数の本体が実行された後に自動的に実行される。
out
ブロック内でassert
チェックが失敗した場合は、関数が契約に違反したことを示す。
これまで説明してきたように、in
ブロックとout
ブロックはオプションである。同じくオプションであるunittest
ブロックも考慮すると、D関数は最大4つのコードブロックで構成される。
in
: 任意out
: 任意do
: 必須だが、in
またはout
ブロックが定義されていない場合、do
キーワードを省略できる。unittest
: オプションであり、技術的には関数の定義の一部ではないが、通常、関数の直後に定義される。
これらすべてのブロックを使用した例を以下に示す。
以下のコマンドでプログラムをコンパイルし、ターミナルで実行できる:
関数の実際の作業は2行だけで構成されているが、その機能をサポートするために、合計19行の重要な行がある。このような短い関数にこれほど多くの余分なコードは多すぎるという意見もあるかもしれない。しかし、バグは決して意図的なものではない。プログラマは、正しく動作することが期待されるコードを常に記述するが、そのコードにはさまざまな種類のバグが含まれてしまうことがよくある。
ユニットテストや契約によって期待が明示的に定義されていると、当初から正しい関数は、その正しさを維持できる可能性が高くなる。プログラムの正確性を高める機能は、すべて最大限に活用することをお勧めする。ユニットテストも契約も、その目標を達成するための効果的なツールだ。これらは、デバッグに費やす時間を削減し、実際にコードを書く時間を効果的に増やすのに役立つ。
式ベースの契約
in
およびout
ブロックは、任意のDコードを許可するのに便利だが、通常、前提条件および後条件チェックは単純なassert
式にすぎない。このような場合に便宜上、より短い式ベースの契約構文が用意されている。次の関数を考えてみよう。
式ベースの契約では、中括弧、明示的なassert
呼び出し、およびdo
キーワードが不要になる。
out
契約では、関数の戻り値がセミコロン前に指定されていることに注意。戻り値がない場合や、out
契約が戻り値を参照していない場合でも、セミコロンは必ず指定する必要がある。
契約プログラミングを無効にする
ユニットテストとは対照的に、契約プログラミング機能はデフォルトで有効になっている。‑release
コンパイラスイッチは、契約プログラミングを無効にする。
‑release
スイッチでコンパイルされたプログラムでは、in
、out
、およびinvariant
ブロックの内容は無視される。
in
ブロックとenforce
チェック
assert
およびenforce
の章で、assert
チェックとenforce
チェックのどちらを使用すべきかを判断するのが難しい場合があることを説明した。同様に、in
ブロック内でassert
チェックを使用すべきか、関数本体内でenforce
チェックを使用すべきかを判断するのが難しい場合もある。
契約プログラミングを無効にできるということは、契約プログラミングはプログラマーのエラーから保護するためのものだということを示している。そのため、この決定は、assert
およびenforce
の章で見たのと同じガイドラインに基づいて行うべきだ。
- チェックがコーディングエラーを防止するものである場合は、
in
ブロックに記述すべきだ。例えば、その関数がプログラムの他の部分からのみ呼び出され、その機能の実現に役立つ可能性が高い場合、パラメータの値は完全にプログラマーの責任となる。そのため、そのような関数の前提条件は、in
ブロックでチェックする必要がある。 - 無効なパラメータ値など、その他の理由で関数が何らかのタスクを実行できない場合は、
enforce
を使用して例外をスローする必要がある。この例を見るために、別のスライスの真ん中のスライスを返す関数を定義しよう。この関数は、モジュール自体で使用される内部関数ではなく、モジュールのユーザーが使用する関数であると仮定しよう。このモジュールのユーザーは、さまざまな、そしておそらくは不正なパラメータ値でこの関数を呼び出す可能性があるため、関数が呼び出されるたびにパラメータ値をチェックするのが適切だろう。プログラムの開発時にのみチェックし、その後
‑release
によって契約が無効化される可能性がある場合は、それだけでは不十分だ。そのため、次の関数は、
in
ブロックでassert
チェックを行う代わりに、関数本体でenforce
を呼び出してパラメータを検証している。out
ブロックでは、同様の問題はない。すべての関数の戻り値はプログラマーの責任であるため、後条件は常にout
ブロックでチェックする必要がある。上記の関数は、このガイドラインに従っている。 in
ブロックとenforce
のどちらを使うかを決める際に考慮すべきもう1つの基準は、その条件が回復可能かどうかだ。コードの上位層で回復可能であれば、enforce
を使って例外をスローするほうが適切かもしれない。
演習
サッカーの試合の結果に応じて、2つのチームの合計得点を増加させるプログラムを書け。
この関数の最初の2つのパラメータは、各チームが獲得した得点だ。他の2つのパラメータは、試合前の各チームの得点である。この関数は、各チームが獲得した得点に応じて、チームの得点を調整する必要がある。念のため、勝者は3ポイント、敗者は0ポイントを獲得する。引き分けの場合は、両チームに1ポイントずつが与えられる。
さらに、この関数は、どちらのチームが勝ったかを 1(最初のチームが勝った場合)、2(2番目のチームが勝った場合)、0(試合が引き分けた場合)で示す必要がある。
次のプログラムから始めて、関数の4つのブロックを適切に記入しよう。main()
内のassert
のチェックは削除しないで。この関数の動作を示すためだ。
ここではint
を返すようにしたが、この関数からはenum
の値を返すほうがよいだろう。