座談会で「よみやすい、しかも仕様変更しやすいコードを目指そう!」をテーマに企画して、いろいろと書籍などを見ていたら、「契約プログラミング」というアプリケーション開発設計があるそうで、それをassertを使って実現するらしいのだが、改めてassertの使いどころをまとめてみた。
正直、私自身もassertなんてJUnitでのユニットテストの時ぐらいしか使っていない。。。。
はじめに
- この記事は、javaのマニュアルを参考に作成しました。
- javaのドキュメントを参考にして作成していますが、「使いどころ」という観点では、他の言語でも使える内容だと思います。
そもそも、assertってなんだ。
- 条件が**不一致(false)**の場合、AssertionErrorをスローする。
javaのマニュアルには下記のように書かれている。
アサーションとは、プログラムに関する前提をテストできるようにするJavaプログラミング言語の文です。
記述方法
assert 条件
もしくは
assert 条件:詳細メッセージ
記述例
public static void main(String[] args){
// なんか処理
...
assert x != 0 : "xが0は正しくないので、処理継続不可能。";
}
Exception in thread in "main" java.lang.AssertionError: xが0は正しくないので、処理継続不可能。
assertの有効/無効
assertは実行時に有効、無効を切り替えることが出来る。
- デフォルトは無効。
- 様々な詳細レベルでアサーションを有効にするには、-enableassertionsまたは-eaスイッチを使用する。
- 様々な詳細レベルでアサーションを無効にするには、-disableassertionsまたは-daスイッチを使用する。
パフォーマンスを考慮して開発時は「有効」、本番時は「無効」としているプロジェクトがあるらしいが、
開発者の間では本番でも基本的に「有効」にすることを推奨しているらしい。
理由は、assertを無効にする理由がパフォーマンス劣化を防ぐ為らしいが、assertがパフォーマンスに与える影響は少なく、それよりも誤動作のリスクを少なくする為に有効にするほうを優先させること、らしい。
※IOTなどの組み込み系の場合は、リソースの制限が厳しい為、無効にするほうがいいかも。らしい。
-
無効でも「AssertionError」エラーを発生さえたい場合は、AssertionErrorクラスを使う。
if (x == 0) { throw new AssertionError("xが0は正しくないので、処理継続不可能。"); }
なんでassertを使う
- 処置の前提条件が正しいかチェック。
- 変数やオブジェクトの状態がアプリケーション継続不可能な状態になった場合、直ちにアプリケーションを停止させ、誤動作を防ぐ。
assertを使ってはいけない箇所
なんでもかんでもassertを使ってはいけないらしい。
publicメソッドの引数値のチェック
publicメソッドの引数の引数値チェックではassertは使ってはいけない。
(これ、やりがち。)
引数チェックはassertの有効、無効に関わらず関数の仕様通り実行されるように保証する為に必ず実施する。
不正な引数値チェックは、適切なExceptionをスローすること。
- IllegalArgumentException
- IndexOutOfBoundsException
- NullPointerException
など
/** コンストラクタ **/
public People(int age) {
// 引数ageは0以上の値であることが前提で処理をコーディングしているので、ここでチェックする
if (age < 0) {
throw new IllegalArgumentException("age:" + age);
}
}
アプリケーションの正しい動作に必要な処理を実行するためにaseertを使用しない
たとえば配列から値を削除する処理で、該当する値がない場合はエラーにする場合、
remove自体をassetに記述すると、assertが無効の場合、remove自体実行されなくなる。
public removeValue(val) {
// 該当する値が有る場合は削除、無い場合はエラー
// これだとassertが無効の時、remove自体が実行されない
assert dataList.remove(val) : "該当データがない データ:" + val;
}
public removeValue(val) {
boolean exists = dataList.remove(val);
// 該当する値が有る場合は削除、無い場合はエラー
assert exists : "該当データがない データ:" + val;
}
assertを使いどころ
内部分岐で想定外の値で処理継続不可能な場合
「ある変数が1,2であること」のような前提があってコーディングすると思うが、その前提が正しいかチェックする。
でも、どこまでチェックしたほうがいいか悩む。。。。
そもそも、その分岐が通るケースがあるのか?
その分岐が通るということはbug??。あー、ロジックを検証する為にも書いた方が品質上がるのかな。
Unitテストケースに「flgがFLG_VALUE1,2以外になったとき」があったら、assert書かないとテストできないな。
// なんかflg値を求める処理
...
if ( flg == FLG_VALUE1 ) {
// FLG_VALUE1時の処理
else if (flg == FLG_VALUE2) {
// FLG_VALUE2時の処理
else {
// 想定外の値、処理継続不可能
assert false : "flg:" + flg;
}
// なんかflg値を求める処理
...
switch ( flg ) {
case FLG_VALUE1:
// FLG_VALUE1時の処理
case FLG_VALUE2:
// FLG_VALUE2時の処理
default:
// 想定外の値、処理継続不可能
assert false : "flg:" + flg;
}
内部処理で求めた値が処理継続不可能な場合
分岐と同じで、あることを前提にコーディングすると思うが、
その前提が正しいかチェックする。
でも、これもどこまでチェックしたほうがいいか悩む。。。。
// なんかcount値を求める処理
...
// count は0より大きい値である必要がある
asset count >= 0 : "countが0以下";
// countを参照してなんか処理
int average = total / count; // countが0だと落ちる。。
...
privateメソッドの引数値のチェック
privateメソッドについては、引数値チェックはassertを使う。ヘルパーメソッド等の内部処理の引数チェックはassertが適している。
private getCount(List<int> array) {
assert array != null : "arrayがnull";
// なんか処理
...
}
メソッドの戻り値のチェック
public、privateに関係無く、戻り値チェックはassertを使ってテストする。メソッドの仕様通りの値を返さない場合はbugを発生させるので処理継続不可能とさせる。
どこまでチェックすべきか悩む。。。ありえない場合は書かなくていいような。分岐のようにロジックを検証する為にも書いたほうがいいのかな。
/**
* コメントの説明文
* @return XXXの時はFLG_VALUE1, XXXの時はFLG_VALUE2
*/
public getFlgValue(int code) {
// なんか処理
...
assert flg == FLG_VALUE1 || flg == FLG_VALUE2 : "戻り値が不正 flg:" + flg;
return flg;
}
オブジェクト状態の事前、事後チェック
- メソッド処理が開始される時点で、オブジェクトの状態が正しい状態であるかチェックする。
- メソッド処理が完了した時点で、オブジェクトの状態が正しい状態であるかチェックする。
class ClassA {
private boolean inited; // 初期化済み
public init() {
// オブジェクトの状態チェック
assert !inited : "初期化済み"; // 処理処理を重複してcall??
// なんか処理
...
assert inited : "初期化済みにしていない";
}
}
おまけ
なんでassert使うの?thow new Exception(...)でよくない?
マニュアルから抜粋
- Exceptionの場合、条件を記述する時、ifや3項演算子を使う必要がある。
- 開発環境、本番環境で前提のテストの無効、有効を切り換える場合、プログラムの変更、もしくはその仕組みを構築する必要がある。
assert時のメッセージは詳細に
例外エラーが発生した場合、たよりになるのはlogのみ。logを見ても解決できないようなlogなら意味がない。
「エラーが発生しました」だけだと、メソッドがどのように処理され例外エラーになったか追えない。
ログに出力したほうがいい項目
- メソッドの引数の値。
- 例外エラーとなった変数の値。
- 例外エラーとなった変数の値を導出する為に使用した変数の値。