契約プログラミング(けいやくプログラミング、)または契約による設計(けいやくによるせっけい、)は、ソフトウェアの正確性 において、ソフトウェアの正確さはと定義されている。と頑健性 において、ソフトウェアの頑健さはと定義されている。ここで「異常な状態」とは仕様によって示されていない状態を指す。を高めるためのソフトウェア設計の方法論である。DbC はロバート・フロイド、アントニー・ホーア、エドガー・ダイクストラらの形式的検証の仕事を基礎にしている。DbC は(抽象データ型に基づく)オブジェクト指向プログラミングにおける表明の利用や、継承に伴う表明の再定義の原理的規則、例外処理の原理的規則などを提供する。
DbC は、バートランド・メイヤーによって提案されたMeyer, Bertrand: Design by Contract, Technical Report TR-EI-12/CO, Interactive Software Engineering Inc., 1986Meyer, Bertrand: Design by Contract, in Advances in Object-Oriented Software Engineering, eds. D. Mandrioli and B. Meyer, Prentice Hall, 1991, pp. 1–50Meyer, Bertrand: Applying "Design by Contract", in Computer (IEEE), 25, 10, October 1992, pp. 40–51, also available online。
概要 「契約による設計」(DbC)における中心的な概念は、クライアントとサプライヤクライアント(顧客)とサプライヤ(供給者)の用法については例えば を参照。邦訳では顧客/供給者が用いられているが本項ではクライアント/サプライヤを用いる。の契約 () である。DbC における契約とは、クラスDbC の文脈における「クラス」の定義は例えば を参照。この定義ではクラスは静的な型として表現され、それ自体はオブジェクトと見なされない。また同書ではしばしばクラスを「モジュール」と表現する。Smalltalk の流れを汲むクラスの扱いに関しては例えば を参照。のインスタンス では基本的に「オブジェクト」の語を用い、あらわにインスタンスとは呼んでいない。クラスを型と見なす場合、クラスのインスタンス以外にオブジェクトは存在しないため () 混用しても問題はないが、一般的な文脈を優先して本項ではインスタンスの語を用いる。とそのメソッド では「メソッド」の代わりに「ルーチン」を用いているが、本項ではより一般的な前者を用いる。「メソッド」は Smalltalk 由来の同義語として に示されている。 の利用に関する条件を形式的に表明したものであり、クラス不変条件とメソッドの事前条件および事後条件で構成される。
不変条件、事前条件、事後条件はそれぞれ、クラスの振る舞いを定義するものであり、クライアントに対して公開された(コンストラクタを含む)メソッドに対して適用される。
契約はまたクライアントとサプライヤに生じる義務と利益によって特徴付けられる。 クライアントの設計者にはクラスのインタフェースとして示された事前条件を遵守する義務が生じる一方、事後条件が満たされること期待してよく、反対にサプライヤの設計者はクライアントが事前条件を満たしてクラスを利用することを期待してよい一方、事後条件を遵守する義務が生じる。 このクライアントとサプライヤの双方に生じる義務と、義務を守った際に保証された状態を得られる利益とが現れる点で、プログラミングにおける「契約」と一般的な意味での契約の類似を見出せる。
表明を予め記述しておくことで、実装者はその表明に従ってプログラムを記述することができる。一般に表明は静的な記述であり、必ずしもプログラム動作時に意味を持つものではないが、DbC をサポートするプログラミング言語では、プログラム実行時に表明を違反していないか監視することができる。実行時の表明違反の監視に関連して、例外機構を利用することにより、実行時の表明違反を例外としてクライアントに処理させることができる。
クラス A が、クラス B に関するエンティティ(ローカル変数の定義やメソッド呼び出し)の記述を含む場合、B は A に対するサプライヤ、A は B に対するクライアントとなる。クライアントとサプライヤは同一クラスであってもよく、例えば this を介したメソッド呼び出しが相当する。
事前条件
事前条件 () は、メソッド開始時に保証されるべき条件の表明である。事前条件はメソッドごとに定義され、以下に関する制約を与える:
メソッドの引数 メソッド開始時のサプライヤクラスのインスタンスの状態
メソッド引数に関する事前条件の例として、配列の要素を添字から参照する際、クライアントは指定した添字が配列の範囲に含まれることを保証する必要があることが挙げられる。
インスタンスの状態に関する事前条件の例として、スタックから要素を取り出す操作(pop)に関して、クライアントは対象のスタックが空でないことを保証する必要があることが挙げられる(スタックが空の場合に何らかの値を返すことを認めない場合)。
事前条件をクライアントとサプライヤの間の契約と見なせば、事前条件を満たすことはクライアントの義務に相当し、事前条件が満たされている前提でメソッド本体を設計できることはサプライヤが受ける利益に相当する。またクライアントに事前条件を提示することは、クライアントとサプライヤの間で責任の所在がどちらにあるのかを明らかにすることつながる。事前条件に対する検査をクライアントが行うよう責任分担することで、サプライヤ側で冗長な検査を行うことや、逆に全く検査が行われないことを避けることができる。
事前条件は表明の一種であり、コンパイル時やプログラム実行時に検査することが可能である(詳細な仕様はプログラミング言語ごとに異なる)。
事後条件
事後条件 () は、メソッド正常終了時に保証されるべき条件の表明である。これはメソッド単位で表明される。正常終了とは、例外スロー終了やエラー発生終了ではないことを指す。具体的には以下になる。
メソッド開始時のサプライヤクラスのインスタンスの状態 メソッド正常終了時のサプライヤクラスのインスタンスの状態 クライアントに渡す返り値
事後条件を満たすことはサプライヤの義務になり、もし満たされた場合は事前に決められた状態遷移が果たされて、これはクライアントの利益になる。クライアントは事後条件への作業から解放される。
不変条件
クラス不変条件 では「(クラス)不変表明」と呼んでいる。 () は、クラスが持つ公開 では「エクスポート」と表現している。された各メソッドの開始時と正常終了時に共通して保証されるべき状態についての条件である。ただしコンストラクタ では「 プロシージャ」と表現している。プロシージャの定義は を参照。の呼び出しに関しては、事後条件としてのみ適用され事前条件として保証する必要はない。(引数や返り値などを制約するメソッド個別の事前/事後条件と異なり)不変条件はインスタンスの状態にのみに対する表明である。インスタンスの「状態」はそのインスタンスのすべてフィールドの値によって決まるため、より具体的には、不変条件はフィールドの値に関する表明となる。
不変条件は公開メソッドの事前条件および事後条件として暗黙的に追加される。 不変条件を持つクラスに関して、そのクラスの公開メソッドの呼び出しの際、クライアントはメソッドの事前条件とサプライヤ・クラスの不変条件の両方を満たす義務がある。 またサプライヤは、事前条件(と不変条件)を満たしたメソッド呼び出しに対して、メソッド終了時にそのメソッドの事後条件と不変条件の両方を満たす義務がある。
義務と利益 「契約による設計」(DbC)において、クライアントとサプライヤに課されるメソッド呼び出しの事前条件および事後条件は、クライアントとサプライヤとの間の契約に喩えられる。一般の契約と同様に DbC における契約は、クライアントとサプライヤの遵守すべき義務 () と義務を遵守することで得られる利益 () を記述したものと捉えられる。
クライアントの義務と利益は:
クライアントの義務:メソッド呼び出し時に事前条件を満たすこと クライアントの利益:メソッド終了時に事後条件を満たした状態が得られること
一方、サプライヤの義務と利益は:
サプライヤの義務:メソッド終了時に事後条件を満たすこと サプライヤの利益:事前条件を満たした状態でメソッドが呼び出されること
である。 サプライヤは事前条件を満たすことをクライアントに課すことで、メソッド本体に冗長な検査を記述することを避けられる。
契約における例外の扱い メソッドの表明違反(事前条件、事後条件、不変条件のいずれかを満たさない状況)が生じた際や、OSが異常を検出した際には、それらを例外として処理しなければならない。
例外処理はメソッドを失敗させるか成功させるかいずれかの形で行わなれなければならない。
メソッドを成功させる場合、定義より、不変条件を含むメソッドの事後条件を満たして呼び出し元に制御を返す必要がある。
失敗させる場合、システムの状態をメソッド実行前の状態に戻し、クライアントへ例外の発生を伝えなければならない。例えばデータベース(DB)のトランザクションでエラーが生じた場合、サプライヤはトランザクションを巻き戻した上で DB 操作の失敗をクライアントに伝える必要がある。
メソッド内で定義された例外ハンドラでは、メソッド本体の実行を再開するか、クライアントへ例外を通知して終了するかのいずれかを行う。例外ハンドラからクライアントへ制御を戻す場合、クラスの不変条件に違反してはならない。
契約プログラミングにおいて、形式的には、数値計算におけるオーバーフローやゼロ除算や、メモリ領域の確保失敗、ファイルへのアクセスや書き込みの失敗などは、システムとクライアントとの間の暗黙の契約違反と見なせる。
契約の継承
クラスをモジュールと見た場合、クラスは開放/閉鎖原則にしたがって設計されるべきである。すなわち、クラスのインタフェースの仕様が安定していて、クライアントから見た振る舞いが変わらないようにしなければならない一方で、将来的な機能の追加や仕様の変更を受け入れられなければならない。後者のモジュールの開放性を実現するための方法の一つとして、クラスの継承がある。
「契約による設計」(DbC)では、クラスのインスタンスの抽象的な振る舞い () を不変条件と各メソッドの事前条件および事後条件として定義する同様な定義は例えば で与えられている。。DbC に従ってプログラミングする際、クライアントは、事前条件を満たせば事後条件を満たす状態が得られること期待して、サプライヤクラスのエンティティを記述することになる。一方で、ポリモーフィズム(多相性)のため、クライアントが記述したサプライヤクラスそれ自身が常に実装を提供するとは限らず、プログラム実行時には動的束縛されたサブクラス ではクラス自身とそれを継承するサブクラスを子孫 () と定義し、クラス自身を除いた子孫を真の子孫 () と定義しているが、本項では特に断りない限り、真の子孫の意味で「サブクラス」を用いる。のインスタンスの実装が利用され得る。
サブクラスのインスタンスの振る舞いは前述の通りサブクラス自身の不変条件および各メソッドの事前条件と事後条件によって定義されるが、一方でサブクラスのインスタンスはクライアントと継承元のスーパークラス ではクラス自身とそれが継承しているスーパークラスを祖先 () と定義し、クラス自身を除いた祖先を真の祖先 () と定義しているが、本項では特に断りない限り、真の祖先の意味で「スーパークラス」を用いる。の契約に拘束され、スーパークラスのインスタンスとしても振る舞えなければならない(リスコフの置換原則)。 したがって、クライアントと継承元のスーパークラスの間の契約を実現するため、サブクラスはスーパークラスの不変条件を常に満たさなければならず(したがってサブクラスの不変条件は自身の不変条件とすべてのスーパークラスの不変条件の論理積となる)、またサブクラスの事前条件はスーパークラスの事前条件より弱く(または等しく)、サブクラスの事後条件はスーパークラスの事後条件より強く(または等しく)なければならない。
ここで、「条件 が条件 より強い」とは、 が成り立つなら も必ず成り立ち、逆は成り立たないことを言う。例えば、実数 に対して が成り立つなら常に が成り立つが、逆は成り立たない。この場合、実数 に対する条件 は条件 より強い、と言える。他方「条件 が条件 より弱い」とは、 が より強いことを指す。
契約プログラミングをサポートする言語 Eiffel Ada D言語 Spec#(C#のDbC拡張版) Kotlin Clojure
脚注
注釈
出典
参考文献