契約による設計
事前条件違反をどう扱うか
"可能性その1:顧客に責任を割り当てる。この場合、条件はルーチンの事前条件の一部として表される。"
"可能性その2:供給者に責任を割り当てる。この場合、条件はルーチン本体に、if condition then ...の形式の条件文、もしくは同等な制御構造で表される。"
この例は、check命令が最も役に立ちそうな応用として、代表的なものである。すなわち、特定の事前条件を持つ(ここでは、sqrtは、負でない引数を要求するような事前条件を持つと仮定する)ルーチンを呼ぶ前に、そのような命令を加えなさい。ただし、呼び出しは事前条件を満たしているが、コンテクストからはすぐにそれがわからないと確信した場合に使う。
表明違反規則
実行時の表明違反は、そのソフトウェアにバグがある証拠である。
事前条件違反は顧客側にバグがある証拠である。
事後条件違反は供給者側にバグがある証拠である。
table: 命令的-適用的の対照表(『オブジェクト指向入門』455ページ)
実装(Implementation) 仕様(Specification)
命令(instruction) 表現(Expression)
どのように(How) 何を(What)
命令的(Imperative) 適用的(Applicative)
指示的(Prescription) 記述的(Description)
事前条件と事後条件
コンストラクタで事前条件をifを使って検証して例外をスローする
達人プログラマー
Bertrand Meyerは、契約による設計というコンセプトをEiffelという言語で採用しました{一部はDijkstra、Floyd、Hoare、Wirthや他の先駆者達の成果に基づいています。Eiffel自身についての詳細は[URL12]と[URL13]を参照してください。}。これは、ソフトウェアモジュールの権利と責任を文書化(そして承諾)し、プログラムの正しさを保証するための簡潔かつパワフルな技法です。では、正しいプログラムとは一体何でしょうか?これは、要求されたこと以上のことも、それ以下のことも行わないというものです。要求の文書化および検証は、契約による設計(Design by Contract/以下DbCと省略します)の核となるものです。 #達人プログラマー 123ページ DbCを使用する最大の利点は、要求事項に制約を課すとともに、それを将来にわたって保証できるところにあります。設計時に、入力値が取り得る範囲、境界条件、ルーチンの結果に関する保証事項、また結果に関して何を保証しないかという点を列挙するだけで、より良いソフトウェアの記述に向けて大きな一歩を踏み出したことになるのです。これらを認識しない限り、多くのプロジェクトの開始、終了、失敗時に見られる「偶発的プログラミング」の罠にはまり込んでしまうのです。
コード中にDbCを記述できない言語でも手はあります。そしてその手は、そんなにまずいものでもありません。DbCとは、詰まるところ設計技法なのです。 自動的なチェックができなかったとしても、コメントとしてコード中に契約を記述しておけば、実際上の利益を享受できます。 トラブル発生時にコメント化された契約があれば、そこを起点に調査を開始できるのです。
表明
このように、仮定をドキュメントにしておくことは大きな一歩ですが、契約をコンピュータにチェックさせることができれば、そのメリットはより大きなものとなります。言語によっては表明を使って、こういったことを部分的に実現できます。なぜ部分的なのでしょうか?DbCができることすべてを表明で実現できないのでしょうか?
残念ながら答えは「ノー」です。まず第一に、継承階層を下って伝播していく表明というものはありません。(中略)
また表明には「古い」(引用者注:Eifellにおけるold)値、つまりメソッド処理の開始時点での値という、組み込みの概念が存在していません。
オブジェクト指向入門
非冗長性の原則
どんな事情があっても、ルーチンの事前条件にあたるテストを、ルーチンの本体で行ってはならない。
この規則は、ソフトウェア工学あるいはプログラミング方法論の多くの教科書で、しばしば「防御的プログラミング(defencsive programming)」という名の下に主張されていることとは逆である。防御的プログラミングとは、信頼性の高いソフトウェアを得るために、システムのすべてのコンポーネントは、できるだけ自己防衛をするように設計されなければならないとする考えである。検査は不十分であるよりは多過ぎるが良いというのが、このアプローチである。冗長性のある検査は、救いにはならないかもしれないが、少なくとも損傷させることはないだろうと。
契約による設計は逆の観点から始まる。つまり、冗長性のある検査は損傷を与える可能性があるだけでなく、実際、損傷を与える。もちろん、一見、これは奇妙に思える。余計な検査(例えば、呼び出すものが x>=0 を保証するように命令されているのに、ルーチン sqrt に、 x<0 にあたる条件命令テストが含まれる)は、最悪、無駄ではあっても、ダメージを与えるわけではないだろうというのが、当たり前の反応である。しかしながら、そのようなコメントは、 sqrt のような個々のソフトウェア要素に焦点を合わせた、信頼性の微視的な理解から生ずるものである。 sqrt の狭い世界に視点を制限するならば、足りないよりは過剰なテストでルーチンがより堅固になるように思える。しかし、1つのシステムの世界が一個のルーチンに制限されるわけではない。たくさんのクラスのたくさんのルーチンを含んでいる。信頼性の高いシステムを得るためには、微視的な観点から、アーキテクチャ全体にわたる巨視的な観点に、視点を転じなければならない。
このようなグローバルな見方をするとき、シンプルさ(simplicity)が重大な判断基準となる。この章のはじめに述べたように、複雑さは品質の敵である。この観点で見直すならば、冗長な検査はもはや無害とはいえないだろう!中規模システムの何千というルーチン(あるいは、大規模システムの何十万というルーチン)を推定した場合、一見、無害のように見えた sqrt の if x <= 0 then... が、だんだん無駄な複雑さの怪物のように見え始める。冗長になりそうな検査を加えることで、さらにソフトウェアが追加される。ソフトウェアが増えれば増えるほど、ますます複雑になることを意味する。特に、間違いの可能性のある条件式のソースが増えると、その結果、さらに検査が必要となり、ソフトウェアがますます増える。これが無限に続くのだ。この道を出発するとき、これだけは確かにいえる。すなわち、信頼性を得ることは決してないだろうと。書けば書くほど、もっと書かなければならなくなる。( オブジェクト指向入門(OOSC) 444-445) 表明は入力検査メカニズムにあらず
陥りやすい誤解を避けるために、ここで確認しておこう。これまで話題にしてきた契約は、それぞれルーチン間(供給者と顧客)で取り交わすものである。つまり、関心があるのはソフトウェア動詞のコミュニケーションであって、ソフトウェアと人間のコミュニケーションでもないし、ソフトウェアと外界のコミュニケーションでもない。また、事前条件はユーザ入力の面倒を見るわけではない。(例えば、対話形式でユーザが入力を行うとき、正の数を入力することを期待する read_positive_integerルーチンのように)次のような形式の事前条件をルーチン内に含んだからといって、これは単なる希望的観測であって信頼性の技法ではない。ここには、お決まりの条件検査の構成要素(古めかしいif...then...を含む)に代わるものはない。こういった場合、次の章で学ぶ例外処理メカニズムが役に立つだろう。
入力データの検証に関するこの問題を解決するのに、表明は果たすべき役割を持つ。モジュールの保護性の基準線内で、センサ、ユーザ入力、ネットワークなど外界から得られたオブジェクトすべての検証を推奨するやり方がある。その場合、オブジェクトのソースのできるだけ近くで行う【近くなければ、実際にルーチンが呼び出されるまでの間に状態が変化している可能性があるからだろう。引用メモ】。また、必要であれば「フィルタ(filter)」モジュールを使う。 #オブジェクト指向入門(OOSC) 446-447 https://gyazo.com/d2d6fe18ee0cb076ae2e947e2316c236
外界から得られた情報(グレーで示したコミュニケーションパス)では、事前条件をあてにできない。しかし、図11.1の中央に薄いグレーで示された入力モジュールのタスク部分は、正しい処理に必要な条件を満たさないならば、右(システムの実際の計算に責任のあるモジュール)に情報を受け渡さないことを保証するものである。このアプローチでは、右への黒い点線で示されるソフトウェア同士のコミュニケーショパスに表明が広く使われる。入力モジュールのルーチンで達成される事後条件は処理ルーチンによって課せられた事前条件に適合する(もしくは、前に示した「強い(stronger)」の意味で、それに勝る)ものでなければならない。 #オブジェクト指向入門(OOSC) 447ページ Eiffel
Java等のassertに対応する表明命令はcheck命令
code:STACK1.e
feature -- 要素を変更する
remove is
-- 最上部の要素を排除する
require -- *事前条件*
not empty
do -- *処理本体*
...
ensure -- *事後条件*
not full
count = old count - 1 -- *"old" はルーチン実行前の値*
end
end
Eiffel provides syntax for expressing preconditions (require), postconditions (ensure) and class invariants (invariant), as well as other assertion constructs studied later (see "Instructions" ): loop invariants and variants, check instructions.