preconditions, postconditions and assertions

とりあえず思い付きをメモ。

前提として、ゲームやツールアプリ開発にて、バグが発生しても動き続けるよりバグの発生自体を防ぐという方向性を想定。

事前条件と事後条件チェックの面倒な点は、条件が満たせなかった場合に理想的には全部実行時エラーで落とせばいいのだが、性能要件等の充足を阻害する可能性がある点。なので、javaJVM オプションで -ea を付けるか付けないかなどの二択というような方式をアーキテクチャ設計に組み込むと普通に死ねる。

ということで、事前条件と事後条件やアサーションの扱いは、アーキテクチャレベルでガチガチに決め込んでおいたほうがいい気がする。

ちなみに、アサーションについていろいろな考えがあると思うが、以後、プロダクション中では実行されないものとする。

で、何を決め込んでおいたほうがいいのかという話だが、項目は結構多岐に渡るので、実際に採用する場合の例を考えてみる:

  • メソッドとコンストラクタには、事前条件と事後条件が考えられる場合は必ず設定する。
  • 事前条件と事後条件は、条件を満たさないこととバグであることが同一である場合のみを指す。*1
  • 事前条件と事後条件は、基本的にはアサーションとしては扱わず、実行時エラーで落とす方式を採用する。しかし、性能要件充足を阻害する場合はアサーション方式を採用する。
  • バグ時でも強引に実行継続するするような場合はアサーションと例外ハンドリングは併用はせず、バグ発生もユースケースとして通常の例外ハンドリングに含める。*2
  • (メソッドやコンストラクタの呼び出し階層に関して)自身の階層のコード内部で困らないものは条件に含めない。*3

上記のような決め事をした際のメリットとかその他考察:

  • 事前条件と事後条件が自然言語ではなくプログラミング言語で書かれることにより曖昧さがなくなる。
  • 事前条件や事後条件を記述すべきかどうかの判断が不要になる。*4
  • 事前条件と事後条件を常に考えることにより、設計ミスが減る。*5
  • アーキテクチャレベルで決まっているのでコードに統一性が出る。*6

その他考察:

  • 内部コードや信頼できるソースからの入力の場合はアサーションは必要ないという考え方もあるが、これって微妙な気がする。なぜなら、呼び出し側のコードがわからなければその判断できないはずであって、その判断ができるということは、呼び出し側のコードとの coupling を意味するので。*7

という感じで、とりあえずメモしてみたw

*1:例外処理や条件分岐などは含まれない。それらは通常のユースケースとして扱う。

*2:二重管理による変更管理コスト増と変更管理失敗時のリスクがあるため。

*3:例えば userName という String 型のデータや age という Int 型のデータが引数にある際に、UserName や Age クラスとしてバリデーション済みが保証されたインスタンスを扱うのが理想だが、現実的にはそこまでやることは少ない。そのような場合、ユーザー入力やファイルや外部システムから受け取るような場合は受け取る処理自身の責務としてバリデーション(この場合はバグチェックじゃないのでアサーションではない)が入るが、それ以外の場所で呼び出しがあるたびにバリデーションしていたらきりがない。っていうか、そこまでやるなら普通にクラス化すべき。クラス化しない場合は、バリデーションされてないと階層内のコード断片の視点で自身の使命を果たせなくなる場合は事前条件や事後条件を入れないようにする。自身の使命が果たせない場合の例としては、Int を受け取りそれで数字を割るような場合は非0の事前条件を入れるとかは必要。自身の使命が果たせる例としては、アプリとして年齢が10-100歳という前提の時に、10年後の年齢を計算するコードがあった場合、そのコード自身は10-100歳という範囲を知る必要が無いし計算にも影響が無いので、事前条件を設定する必要が無い。ちなみに、『影響が無いから便器的に無視するのであって本来はチェックしたほうがいいんだよね?』という問いにはNO。これは常に無視するのが理想。なぜなら、そのような意味的(semantic)な関連性を考慮していたら、それらのコードは意味的に関連を持ち、結合度が強まってしまう。この syntactic ではないが semantic な coupling というのが実はアプリ開発で最も保守性を低下させるもの。具体的には、10-110歳までの範囲にする仕様変更があった場合に変更箇所どうなるの?コンパイラは教えてくれないよ?コンパイルエラーにもならず実行時エラーにもならないバグになるよ?という話。

*4:絶対に書くというルールなので

*5:特にドメインモデル設計の後半で実コードを書きながら考えられるというのは大きい。

*6:例えば、実行しない、開発時のみ実行(デフォルト)、常に実行、というオプション付きの assert 関数を用意してそれを利用するとか、それを利用した際の例をアーキテクチャ設計書に入れるとか。アーキテクチャ設計書が例ではなく実際のコードを自己参照してしまえば設計書の記述が風化することも無いし(リリースの際に設計書を一字一句全部レビューするという前提が必要だが)。

*7:アサーションを行うということは他のどこかでチェックされているという前提なので、それならそもそも正しいデータであることを保証する interface を用意することによりアサーション自体が必要無くなるはず。例えば、住所の文字列フォーマットをチェックしたいなら、システムへの入り口で Address interface としてチェック済み文字列を用意してしまえば以降のチェックは必要無くなるはず。