$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

「オブジェクト指向入門 第11章 契約による設計」を読んだ

これまでの続きで、「オブジェクト指向入門 第11章 契約による設計」を読んだ。「オブジェクト指向入門 第6章 抽象データ型」を読んだ - $shibayu36->blog; で紹介した抽象データ型と同様に非常に面白い章であった。プログラムの設計を考える時に役立ちそうな知識が自分の中で言語化されたので、今後の設計時や、コードレビューの指摘の時にも役に立ちそう。


この章は信頼性の高いソフトウェアを記述するために、表明という概念を解説してくれる。例えば、ソフトウェアが正しいとは何かについての議論や、事前条件・事後条件・クラス不変表明のような表明の種類の説明、それぞれの表明の役割、表明の使いみち、表明の監視などについてのトピックがあった。

僕はこの章を読んでいて、

  • 事前条件と事後条件による義務と利益
  • 表明を満たさない時にどうするべきか

という部分が印象に残ったので、この話題について自分なりの言葉でメモしておく。自分なりの言葉でメモしているので、もしかしたら解釈が間違っている場合があるので、その時は指摘してもらえるとありがたいです。

事前条件と事後条件による義務と利益

事前条件、事後条件とは

まず事前条件とは、あるルーチンを適切に機能させるための制約を表したものである。例えばSTACKの例であれば、「スタックがいっぱいならばputは正しく機能しない」などといったものがある。抽象データ型のPRECONDITIONSに対応している。

事後条件とは、事前条件を満たした状態でルーチンを呼んだ場合に、そのルーチンが実行した後に保証される性質を表したものである。例えば、putの実行後には、「スタックは空でない」、「スタックの一番上の要素は今putされたものである」、「要素数は1増加している」という性質を満たす、というもの。抽象データ型の記述のFUNCTIONSやAXIOMSという部分を実装に落とし込んでいくと、事後条件となる。

事前条件と事後条件による義務と利益

ルーチンに対して事前条件と事後条件を定義するということは、ルーチン(提供者)と、それを呼び出すもの(顧客)の間で契約を結んだ、と言える。契約文書に直すと、「もし顧客が事前条件を満たした状態でルーチンを呼んでくれるならば、事後条件を満たす状態を最終的に実現することを約束します」という契約を結んでいるということになる。

契約を結ぶということによって、顧客側、提供者側、双方に義務と利益を与えることとなる。顧客側、提供者側のそれぞれの義務と利益は以下のとおり。

  • 顧客(ルーチンを呼び出す側)は、事前条件に示されている制約を必ず満たした上でルーチンを呼び出す義務がある。その代わりにそのような呼び出しをすれば、ルーチンの実行後必ず事後条件のような状態になることを提供者が保証してくれるという利益を得られる。
  • 提供者(ルーチン)側は、事前条件が満たされた状態でルーチンが呼ばれたならば、ルーチンの実行後に必ず事後条件のような状態にする義務がある。その代わり、顧客が事前条件を満たしてくれるため、その状態は保証されていると考えて、様々な状態について考慮する必要がなくなり、ルーチンをシンプルに記述できるという利益を得られる。

このように契約を正しく結ぶことによって、顧客側、提供者側双方に利益をもたらすことが出来る。またきちんと義務を明文化することにより、バグがあった時にどちらの責任かをはっきりさせることが出来る。バグが起こった時に、もし顧客が事前条件を満たさない状態でルーチンを呼んでいるならそれは顧客の責任であり、事後条件を満たさない状態で返しているならそれは提供者の責任である。


この辺りの説明を読んだ時、僕はこの考え方は非常に役に立つと感じた。設計の時に、渡ってきたものが妥当かをどこまでチェックすべきか、どちら側でチェックするかなど迷うことが多い。そのようなときは、まず顧客側、提供者側ではっきり区別し、この事前条件というものを考え、どちらに持たせるかということを考えれば良さそう。さらに事後条件をはっきりと考えることによって、顧客側で結果について無駄にチェックするということも防げる。

また顧客、提供者の関係はクラス間だけではなく、同じクラス内のメソッド間にも適用できるように見え、それもどちらで保証するかを考えるための参考になりそうと思った。

表明を満たさない時にどうするべきか

事前条件や事後条件、またクラス不変表明といった表明の種類は分かった。ただ、それを見た時に「ではそれが満たされない時にどうなると良いのか」という疑問を持った。特に事前条件を満たさなかった場合でもルーチンを実行してしまうと、そのルーチンを実行しただけでデータが壊れてしまう場合があるが、どうすればよいのか。データが壊れる例としては、配列の範囲外アクセスをすることにより、別のメモリ空間を壊してしまう、などといったことである。

このことについては、「実行時に表明を監視する」という項に言及があった。表明を定義したら、それを監視する必要があり、その監視をどこまでするかは実際のクラステキストを変えなくても、設定ファイルなどにより変更できるべきと書かれていた。例えば設定ファイルで全ての表明をチェックするようにしておくと、事前条件・事後条件・クラス不変表明などが満たされない場合に全て検知してくれる。このようにしておくことで、環境によって事前条件のみチェックするようにするなどの事ができる。

またさらに、ではどこまで監視を有効にしておくべきかという疑問も持った。監視することは信頼性を向上させる一方でコストの問題(パフォーマンスの劣化など)にもつながるので、どこまでやるべきなのか。これに関しては以下の様なことが書かれていた。

  • 開発時やデバッグ時には全ての表明を監視させるべきである
  • 効率重視のアプリケーション領域(100万分の1秒の時間が惜しいとか)で、システムを十分信頼している場合は、全ての監視をとっても良いだろう
    • ただし、表明を監視しなければ、システムを十分に信頼することはめったに出来ない(実行中に無限にある可能性をテスト時に網羅することは不可能だが、意図しないケースでも正しく動くことを信頼できるのか)
  • 事前条件のみをチェックするだけでも、ルーチンの呼び出しで悲劇的なことになる(例えばデータを壊すとか)ことを防ぐことができ、さらにそのチェックコストは大変少ないので、一つの可能性として面白い
    • クラス不変表明は全てのメソッド呼び出しでチェックしなければならないのでコストが大きい


以上のことが書かれていたので自分が疑問に思っていたことはある程度解消された。しかし監視でエラーが検知された時に実際にシステムはどうなれば良いのか、についてはこの章で特に扱われていなかった。その部分に関してはまだわからないが、次の章のタイトルが「契約が破られるとき:例外処理」というものなので、そこで明らかにされるだろう。

まとめ

今回は「オブジェクト指向入門 第11章 契約による設計」を読んで、自分なりに印象に残ったところをまとめてみた。「第6章 抽象データ型」の章のもう少し実践よりの章という位置づけで、6章と同じくらい参考になった。特に事前条件・事後条件といった表明の話と、その表明を満たさない時の選択肢について解説されていた部分が面白かった。

次の章は「契約が破られるとき」にどうするかが書かれているようなタイトルなので、楽しみに見たいと思う。また早く継承に関する章が読みたい。