validation と sanitization

ふと思ったのでメモ。

validation/sanitization 済みであることを示す interface が結構便利。*1

1か所でも validation/sanitization を行う個所があるならオブジェクトとして扱う方向を考えたほうがいいかも。なぜなら validation/sanitization のロジックをカプセル化する場所になるので。

*1:基本的には validation/sanitization 対象が不変かつ対象のデータに閉じて判断可能な場合のみ可能だが、全てを静的に判断できない場合でも静的に判断可能なチェックのみ完了したこと(例えば住所文字列フォーマット準拠とか)を表す interface を用意するのは有用。

robustness と correctness

robustness は要件として検討されるもので、robustness を要件に含めるという文脈でのみ correctness を検討する必要が出てくるんじゃないかと思う。また、基本的には robustness を要件に含めると複雑さや保守コストがかなり増す*1ので、よほど特殊な事情でもない限りは robustness を要件に含めるよりバグのないシンプルな設計を志したほうがいいと思う。*2

結論:予想外*3のエラーの唯一のハンドリング方法は問答無用でシステムを落とすことw*4

*1:非機能要求から機能要求に転化した上に仕様変更が頻繁する可能性もある。また、データのバグを握りつぶして動かしことにより不正なデータがDBに入りそのデータがシステム全体のデータに影響を与えた後に発覚した時のマイグレーションは通常はもう手遅れだし、それが課金やシステム内の通貨やポイントだったりした場合はユーザーへの金銭的補償なども発生する可能性がある。実際に、過去においらがその危険性を何度も忠告したのに対策せずに問題を起こした大規模プロジェクトがあった、、、。

*2:結局はユーザーの要求次第なのですがw

*3:どんなにレアな事象でも起こる可能性があるものは予想外じゃないのでエラーではなく例外としてハンドリングすべき。

*4:現実としてはオンメモリでユーザーがデータ編集中の場合とか分散環境の大規模システムの場合などはそうもいかないので、落とす前に編集中データをバックアップするとか、エラー影響範囲を局所化してそこだけ落とす設計とかが必要。特に、不正なデータがそのまま利用されるのだけは確実に阻止することが重要。

The Wise Developers’ Guide to Static Code Analysis featuring FindBugs, Checkstyle, PMD, Coverity and SonarQube

zeroturnaround.com
ちょっと古い記事だけどよくまとまっているのと彼ら自身の経験談でもあるのでメモしておくw

ZeroTurnaround uses SonarQube for our Java projects JRebel and LiveRebel, as we find that it gives us the best combination of usefulness and transparency against the relative complexity of setting it up and solving weird configuration issues.

kotlin における例外について

ふと思ったのでメモ。

  • 例外なくアーキテクチャレベルでの扱いとなり、アーキテクチャ設計書で解説されるべきとか。
  • java で言うところの検査例外は sealed class の戻り値で扱うとか。
  • 検査例外のバケツリレーが発生するような場所はアーキテクチャレベルで実行時例外*1としての扱いを検討するのが良さげとか。*2

preconditions, postconditions and assertions - Be an Idealistic Realist も参照のこと。

*1:kotlin上ならRuntimeExceptionである必要は無いけどjavaとの互換性を考えると実行時例外にしておいたほうが無難だと思われ。

*2:多くの場合、実行時例外の発生可能範囲、ハンドリングを行う境界、具体的なクラスや仕組みなどが定義されると思われ。例えば、エラーが起きてもエラーメッセージだけ表示させてアプリを落とさないようなユーザー要件がある場合に、実行時エラーをスローしてUI側で受けてエラー表示と握りつぶしをする仕組みとか。

kotlin における検査例外的なものについて

ふと思ったのでメモ。

おいらは java では検査例外を結構気に入って使っていたが、kotlin では実行時例外しかないので微妙に困った。しかし、そもそも検査例外に満足していたわけでもない。

基本的にはインタフェースメソッドの仕様はユースケースに近い考え方で、主経路の結果が戻り値、代替経路の結果が例外というような考え方をしている。しかしこれは、様々な側面を考慮したうえで java 言語仕様に合わせた結果こうなったというだけの話で、理想形と言うわけではない。という感じで、いくつか思うところがある:

  • 例外を代替経路の結果としてとらえた場合、常に例外の型だけで代替経路を識別できるわけではない。複数の代替経路で同じ型の値を返したい場合には、代替経路の種類と値の両方の識別が必要となる。kotlin の場合、例外を使わず sealed class で対応し、sealed class の型を経路識別に利用し、経路の網羅性をコンパイラにチェックされるというのが落としどころですかね。
  • 複数の戻り値を得たほうが便利なこともある。純粋関数以外を認めないというのであればそれもいいが、関数は処理の段階的詳細化でも利用される。その際、戻り値が1つだとめんどくさいこともある。例えば x, y, z という int 値を受け取りたい場合、ポインタ渡しのほうがラクチンというようなケースもある。実際はポインタはバグを生みやすいので、kotlin なら destructuring でそれっぽい感じにするというのが落としどころな気はする。val (x, y, z) = hoge() 的な感じで。*1
  • 各経路用のクロージャを渡すという方法も無くはない。副作用のみを期待する場合には実は結構きれいにコーディングできる、、、のだがそれが罠。クロージャ渡しをしたくなる誘惑は主に呼び出し側と呼ばれる側の両方のコードが頭に入っている場合に発生する。例えば、関数Aが関数Bを呼び出し、関数Bの代替経路XとYで個別に処理する場合、代替経路の結果がXであるかYであるかの条件分岐を行ってから処理を行う必要がある。しかし、クロージャを渡してしまえば関数A側は処理を渡してそれでおしまいになる。しかし、クロージャが呼ばれたかどうかの保証は得られないし、クロージャの処理の結果に応じて行うべき処理を後付けしようとしたらクロージャから値を入れるための変数を事前に作るなどの処理が必要となり、コードがかなりカオスになってくる。また、クロージャの処理が実行されるタイミングは関連コードを全てホワイトボックスで同時に把握しない限り判断できないため、クロージャが呼び出されるタイミングが問題になった時に、関連コードすべての coupling が成立してしまう。とかいろいろ問題があるわけだが、コードの実行を時系列で追いづらくなるクロージャは無条件で複雑さを導入するのでそれ以上のメリットが無い場所ではやるべきじゃないと思われ。

とりあえずの結論:

  • 関数を代替経路のあるユースケースに見立てる場合は、例外は使わず sealed class を使い、経路毎に異なる型を割り当てる。*2
  • 関数の戻り値を複数得たい場合や分解して得たい場合は destructuring を利用する。*3
  • 戻り値を判断して対応可能な処理はクロージャ渡しでやらない。

という感じですかね。

*1:hoge()側の戻り値の型をどうするのかはプロジェクトのポリシー次第でしょうね。data class Point3D(val x: Int, val y: Int, val z: Int) のようなクラスを作るのか、Triple<Int, Int, Int> のようにカスタムクラスは作らないで対応するのかなど。可読性や将来的な変更局所性はカスタムクラスのほうが高いと思われる。

*2:when による条件分岐では when を値として利用しない場合も val dummy:Unit = when のように変数に代入して、sealed class の型を網羅していない場合はコンパイルエラーになるようにする方法が結構使える。

*3: val (x, y, z) = hoge() のような感じ。

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 としてチェック済み文字列を用意してしまえば以降のチェックは必要無くなるはず。