フェイルセーフなコードを書くには?

フェイルセーフというのは、機器故障しても安全なほうに倒れるシステムの組み方で、原発の制御棒(BWRはフェイルセーフに反しているが、PWRはそれが解消されている)が重力で落ちるようになっていたり、原子炉自体が半分以上地下にあったり、冷却水が上から下へ流れるようになっていたり(これもBWRはフェイルセーフに反している)する仕組みです。

フェイルセーフ – Wikipedia
http://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A7%E3%82%A4%E3%83%AB%E3%82%BB%E3%83%BC%E3%83%95

ちなみに、フォールレトラントのほうは、故障が起きても処理を継続できる仕組みで、フェイルセーフのように「問題が起きても大丈夫」という点では一緒なのですが、問題発生の後が異なります。

フェイルセーフは、問題発生→安全に機能停止
フォールレトラントは、問題発生→稼働率は下がるが処理を続行

という感じです。
# 情報処理の試験の解答がどうだったか覚えていないのですが…ここで解説するフェイルセーフは「安全に機能停止」のところに主眼を起きます。

さて、フェイルセーフなコードに関しては、過去にもいくつか書いていたりするのですが、再び。

// 指定年齢以上のカラムを取り出す
List<DataRow> dest = SelectMoreAge( src, 20 )
// 指定年齢以上のカラムを取り出すメソッド
List<DataRow> SelectMoreAge( DataTable src, int age )
{
	List<DataRow> dest = new List<DataRow>();
	foreach ( DataRow r in src.Rows )
	{
		int a = (int)r.Item("age");
		if ( a >= age ) {
			dest.Add( r );
		}
	}
	return dest ;
}

こんなソースがあったとき、一発で分かるのは、

・src が null だった場合はどうするのか?
・SelectMoreAge メソッドの戻り値は null になるのか?

のようなチェックです。他にも、パラメータ age の値が

・マイナス値のときは、どうなるのか?
・0 の場合はどうなるのか?
・10000 などの大きな値(年齢とは思えない値)のときはどうなるのか?

というチェックがあります。
# 数値なので、-1,0,10000 でも余り変わらないのですが、例として。

■引数の null をどのように処理するのか?

結論から言うと、フェイルセーフの考え方でいくと、「例外を出さずに、何もなかったように処理を続ける」のが正解です。

・引数が null だから null exception を発生させる。
・引数が null だから、エラーが分かるように戻り値を null にする。

ということは「しません」。

フェイルセーフの「安全に機能停止」を求めようとすると、SelectMoreAge メソッドは処理としては、異常だけれども大きな影響を出さずに止まる、という流れに乗せます。

// 指定年齢以上のカラムを取り出すメソッド
List<DataRow> SelectMoreAge( DataTable src, int age )
{
	List<DataRow> dest = new List<DataRow>();	※2
	if ( src != null ) {						※1
		foreach ( DataRow r in src.Rows )
		{
			int a = (int)r.Item("age");
			if ( a >= age ) {
				dest.Add( r );
			}
		}
	}
	return dest ;
}

まずは、※1 のところで、src の null チェックをします。このときメソッドを使っている側の対応が困らないように、戻り値を ※2 のところ、あらかじめ作っておきます。
SelectMoreAge の呼び出し目的は、指定した年齢以上のデータがほしい訳ですから、0 件を返してやれば、呼び出し側は安全に停止する(数件取得できることを期待するので)ことができます。

呼び出し側では、以下のように null チェックをせずに済みます。と言いますか、null チェックを忘れても動くようになります。

// 指定年齢以上のカラムを取り出す
List<DataRow> dest = SelectMoreAge( src, 20 )
if ( dest.Rows.Count > 0 ) {
	// 数件あるときの処理をする
}

■引数が期待されたもの以外の場合は、どう処理するのか?

引数 src の値が null の時は、明らかにエラーなのですが、年齢が -1,0,10000 の場合は定かではありません。
なので、ある程度期待される範囲を制限して処理すると、より安全になります。

// 指定年齢以上のカラムを取り出すメソッド
List<DataRow> SelectMoreAge( DataTable src, int age )
{
	List<DataRow> dest = new List<DataRow>();
	if ( src != null ) {
		if ( 0 <= age && age <= 100 ) {
			foreach ( DataRow r in src.Rows )
			{
				int a = (int)r.Item("age");
				if ( a >= age ) {
					dest.Add( r );
				}
			}
		}
	}
	return dest ;
}

こんな風に、SelectMoreAge メソッドが 0 以上 100 以下の年齢を対象にしていることを明確にします。
if 文のネストが深くなってしまうので、以下のように書き換えても ok です。

// 指定年齢以上のカラムを取り出すメソッド
List<DataRow> SelectMoreAge( DataTable src, int age )
{
	List<DataRow> dest = new List<DataRow>();
	// src は null でないこと
	if ( src == null ) {
		return dest ;
	}
	// 年齢は 0 以上 100 以下であること
	if (!( 0 <= age && age <= 100 )) {
		return dest ;
	}
	// 処理をする
	foreach ( DataRow r in src.Rows )
	{
		int a = (int)r.Item("age");
		if ( a >= age ) {
			dest.Add( r );
		}
	}
	return dest ;
}

■メソッドの構造を決める

このようなコードを「業務風」に仕上げていくことができます。
メソッド内をブロックに分けて、どのような手続きをしているかをプロジェクトのメンバーで決めておくと、相互にコードに手をいれやすくなります。

// 指定年齢以上のカラムを取り出すメソッド
List<DataRow> SelectMoreAge( DataTable src, int age )
{
	/*******************************************/
	/* 内部変数                                */
	/*******************************************/
	List<DataRow> dest = new List<DataRow>();
	/*******************************************/
	/* パラメータチェック                      */
	/*******************************************/
	// src は null でないこと
	if ( src == null ) {
		return dest ;
	}
	// 年齢は 0 以上 100 以下であること
	if (!( 0 <= age && age <= 100 )) {
		return dest ;
	}
	/*******************************************/
	/* 内部処理                                */
	/*******************************************/
	// 処理をする
	foreach ( DataRow r in src.Rows )
	{
		int a = (int)r.Item("age");
		if ( a >= age ) {
			dest.Add( r );
		}
	}
	/*******************************************/
	/* 戻り値                                  */
	/*******************************************/
	return dest ;
}

このように適度にコメント(飾り罫線)を入れて整形します。
ひとりで書くときは、この手の装飾は必要ないのですが(行数が多くなって、テキストエディタで表示できる処理行が少なくなってしまうので)、複数名で作業する業務コードの場合は別です。

という訳で、安全性を考慮したコードを書く場合には、できるだけ null を返しません。また、例外を返すことも少ないのです(例外を拾わないことにより、アプリケーションエラーになる確率が増えるので)。

カテゴリー: 開発, 設計, UIDD パーマリンク

フェイルセーフなコードを書くには? への2件のフィードバック

  1. k1496 のコメント:

    ちょっと前にあがったnullという値は必要か、を読んで。

    PHPの場合は連想配列を使うことが多いので、例外は0件の配列を
    返してやればうまくいくなぁと思ってました。

    業務のコードはこれくらいコメント書きますよね。
    なんかみんな書かないしネット上のソースもコメントないから
    それが普通なのかと思ってしまう。

    というかPHPにはphpDocumentorがあるので、それに則って書くべきだし。
    変数をどこでも宣言できてしまうので、これも見づらくなる原因だと最近
    思います。

    CakePHPが制約を設けて秩序を維持しているので、なんでも自由というのも
    逆に考えものかなあと。

  2. masuda のコメント:

    null は、明示的に「何もない」ことを指すのが良いかと思っています。
    例えば、0 個の配列を返す場合は、単に 0 個なのか、何らかの意図があって 0 個なのか、が区別できないときに、あえて null を使うというやり方です。
    ただ、フェイルセーフの話で書いたように、エラーのときに null を返すと決めてしまうと、呼び出し側の判別が多くなって、運用時のエラーが増える。なので、0 を返す、ってのが安全なコードの書き方ですね、ということなのです。
    ・・・とか言う話は、なかなか理解して貰えないんですよね~。なぜだろう?

    そうそう、コメントの飾り罫はあまり好きではないけど、業務コードではよくやります。
    適度な空行を入れるか、飾り罫を入れるかという感じ。
    あと、変数の位置は、本当は使う直前で宣言したほうがいいんだけど、分かりづらいっていう人が多いので、関数の先頭に宣言するかなぁ(私はやらんけど)、というテンプレートを使ってます。

コメントは停止中です。