必ず成功するための100の開発手順(メモ)

新規の開発手順を作成中です。まだ50手順弱なので100手順にしようかなと。

>> 要件定義
 >> 機能要件
 >> 顧客要望まとめ
  >> 業務分析
 >> 機能抽出
 >> システム概要設計
 >> リスク抽出
 >> リスク対策
>> 見積
 >> 顧客スケジュール
 >> 期間見積
 >> 配員計画
 >> 金額見積
>> 交渉
>> 設計工程
 >> システム設計
 >> ネットワーク設計
 >> オブジェクト指向設計
 >> データ構造設計
 >> ユーザーインターフェース設計
  >> 画面確認
>> 実装工程
 >> 実装計画
 >> タスク抽出
 >> 内部設計、詳細設計
 >> 実装
 >> 単体試験
 >> 結合試験
>> 試験工程
 >> 試験計画
 >> 機能試験
 >> 負荷試験
 >> ユーザー機能試験
 >> 修正
 >> 不具合表
>> 導入
 >> 新規導入
 >> 移行計画
 >> 運用マニュアル
 >> 運用試験
 >> 試験運用
 >> 運用開始
>> 保守
 >> ユーザーサポート
 >> 初期不良対処
 >> 変更要件
カテゴリー: プロジェクト管理, Plan Language | 5件のコメント

業務コードで使う例外処理のたった1つの方法

いわゆる「○つの方法」という釣りタイトルですが、ワタクシ的には例外処理に関しては一択です。
例外処理については、方法論的にも色々あるのは分かっているのですが、「業務」という範囲で考えると、という選択です。

UIがある場合とない場合で、ちょっとだけ書き方が分かれます。

// UIのある場合
int func(...) {
 try {
  // 全ての処理
 } catch ( Exception ex ) {
  // エラーメッセージを表示
  MessageBox.Show( ... );
 }
}
// UIのない場合
int func(...) {
 try {
  // 全ての処理
 } catch ( Exception ex ) {
  // そのまま例外を返す
  throw ;
 }
}

こんな風に、関数/メソッド内の処理を【全て】try catch で囲ってしまいます。
乱暴ですよね…ですが、この方法って VB3.0 の頃からの鉄板な方法だったりします。

sub func(...) 
ON ERROR GOTO L_ERROR

 ' 内部のすべての処理

 exit sub
L_ERROR:
 ' エラーメッセージを表示
 MsgBox(...)
end sub

のように、VB の頃(.NETではない)は、ON ERROR GOTO を使っていました。
さて、この頃 C++ はどうだったかというと…実は C++ は余り流行ってはいなくて、いまだ C 言語が主流でした。なので C 言語の場合は、

void func( ... ) 
{
	int err ;
	
	err = subfunc( ... );
	if ( err != 0 ) {
		// エラー処理
	}
}

な風に、戻り値でエラーを判別するしかなかったのです。

この鉄板の例外処理でログイン処理を書いてみると、

class Principal {
  string _username ;
  
  public bool Logon( string username, string password ) {
    try {
      if ( username == "" ) 
        throw new Exception("ユーザー名が空白");
      if ( password == "" ) 
        throw new Exception("パスワードが空白");
      if ( username == "root" ) 
        throw new Exception("rootユーザーはログインできない");
        
      SqlConnection cn = new SqlConnection(...);
      SqlCommand cmd = new SqlCommand( cn, 
        @"SELECT count(*) from usres 
          where @username = username and @password = password" );
      cmd.Parameters.Add(new SqlParameter("@username", username ));
      cmd.Parameters.Add(new SqlParameter("@password2, password ));
      cn.Open();
      int cnt =(int)cmd.ExecuteScaler();
      cn.Close();
      if ( cnt == 0 ) 
        throw new Exception("ログインできない");
        
      // ユーザー名を保持して正常終了
      this._username = username ;
      return true;
    } catch ( Exception ex )
      throw ;
    }
  }
} 

なにやら妖しげなコードになりますが(苦笑)、業務コードの場合はこっちのほうがすんなり通るし頑健なのです。

実は、このコード例外の throw が大量に書いてあるので安全なコードとは程遠いような気もしますが、ライブラリとしての使い方ではなくて業務コードをこのスタイルにしていまうと、exception の catch が必須になるので、これはこれで安全なのです。

フェイルセーフなコードを書くには? | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2173

さて、この業務コードを利用する側も try catch を多用します。

/// ログインボタンのクリック
protected void Login_OnClick(...)
{
	try {
		Principal user = new Principal();
		string username = TextUserName.Text ;
		string password = TextPassword.Text ;
		// username, password のチェックは一切しない。
		user.Logon( username, password );
		// 成功時にフォームにユーザー権限を保持する
		this.User = user ;
	} catch {
		// 例外発生は、一律「ログインできない」とする。
		MessageBox.Show("ログインできませんでした");
	}
}

こういう書き方を一律すると「正常系」を一気に書くことができます。
この発想のベースは、VB3.0 の頃からある ON ERROR GOTO なんですけどね。

こんな風な例外処理を取る場合、いくつかの前提条件があります。

・データベースアクセスが基本、正常に行われること。
・処理の流れが基本、正常系で流れること。
・プログラムのバグは、基本皆無であること(実行時にデバッグログ等を取らない)。
・ユーザーへは基本、アプリケーションエラーを知らせないこと。

ということです。このプログラムの書き方は、プログラムの潜在バグを隠してしまうというデメリットがあるのです。ですが、ワタクシ的には「リリースしたプログラムは、ユーザーの目の前で落ちないことが最優先ではなかろうか?」と考える訳でで、突如として落ちないことを最優先にする(それが潜在バグを隠すものであったとしても)と、こういう例外処理の使い方もありかなぁと考えています。

ちなみに、そこそこの業務以外では、こういう書き方をしません。アリだとは思うのですが、業務コードを書くときに限ってという話です。手元のツールを作るときなんかは DEBUG 定義などを利用します。

カテゴリー: 開発 | 2件のコメント

SQL Server 2000 では文字列を数値に勝手に変換するよ

後で、SQL Server 2008 とかの最新バージョンを調べますが、落とし穴になりかねないのでメモ。

create table test1 (
 id int,
 name varchar(10),
 num int 
);

として test1 テーブルを作成しておきます。
ここに次のようなデータを入れます。

delete from test1 ;
insert into test1 values(1,'masuda',10);
insert into test1 values(2,'tomoaki',20);
insert into test1 values(3,'100',30);

このデータを検索します。

select * from test1 
 where num = 10 ;

この結果は予想通り、1行目が取得できます。

1	masuda    	10

さて、num に文字列の’10’を指定したときはどうなるでしょうか?

select * from test1 
 where num = '10' ;

マッチングしないと思いきや、自動的に文字列型から数値型に変換されて、1行取得されます。
# Oracle の場合は変換エラーになったと思うのだけど、うろ覚えです。

1	masuda    	10

逆に文字列型(varchar)に対して数値で比較してみると、

select * from test1 
 where name = 100 ;

これはエラーになります。

サーバー : メッセージ 245、レベル 16、状態 1、行 1
構文エラー。varchar 値 'masuda    ' から int データ型に変換できませんでした。

1行目の name 列の値「masuda」が int 型に変換できないためにエラーになっていますね。
となると、name 列の値が全て数値型になるようにしたら、どうなるのでしょうか?

delete from test1 ;
insert into test1 values(1,'100',10);
insert into test1 values(2,'200',20);
insert into test1 values(3,'300',30);

というデータを入れた場合に、次の SQL を動かすと、

select * from test1 
 where name = 100 ;

実は、結果が取れるのですッ!!!

1	100	10

そんな訳で、SQL Server 2005 の SQL を書くときに、自動変換をあてにしたり、自動変換がされるような書き方をすると、はまる可能性があるよという話でした。
これは、.NET から扱うときに、SqlParamter オブジェクトの使い方が問題になってくるのですよね。これは別の記事に。

カテゴリー: 開発 | 2件のコメント

純粋オブジェクト指向言語を思考実験してみるテスト

関数言語風なところ(LINQ)や、手続き型の関数の羅列を取り除いて、オブジェクト指向オンリーでプログラミングをする言語を妄想(笑)してみます。個人的には、純粋オブジェクト指向言語は Java がそれに近いのですが、もうちょっと何とかするいう形で。

まず、オブジェクト指向言語の重要な要素を挙げると、

1)カプセル化(隠蔽化)できる。
2)ポリフォリズム(多態化)できる。
3)継承関係がある。

なところです。

ここで、焦点を当てるのは「カプセル化」です。
もともと、オブジェクト指向の最初は、オブジェクト同士のやりとは「メッセージ」を使ってやり取りをしています。

objectA -> message -> objectB

のような繋がりになります。このメッセージ(message)は、objectA のメソッドなのか、objectB のメソッド(リスナー?)なのかは、定義されません(とか思っています)。

また、メソッド(関数)の呼び出しは、C言語やC++の表記上、複数の引数とひとつの戻り値、という決まりがありますが、実は戻り値は複数でも構わないのです(F# のタプルと同じですね)。

objectA.Method
{
  (retA, retB, ... ) = objectB.Method(paramA, paramB, ... )
}

な形です。

さて、手続き型の流れの場合は、if 文を使ってフロー制御をしますが、純粋オブジェクト指向の場合は if 文の扱いは、ちょっと慎重になります。

if ( a != null ) {
  if ( b != null ) {
    objB.Method( a, b );
  }
}

のように呼び出し前にチェックを入れますが、純粋オブジェクト指向では呼び出し時のチェックをしません。
これは、パラメータの整合性のチェックは、呼び出されたオブジェクト側の責務であり、呼び出し側では判断がつかないためです。もう少し詳しく言うと、隠蔽化を進めるとどのパラメータが正常値であるのかは、呼び出されたオブジェクトしか分かりません。

なので、責務を明確にするために、

// 呼び出し側では、パラメータチェックをしない
objB.Methd( a, b )

// objB の中でパラメータチェックをする
void objB.Method( string a, string b ) {
  if ( a == null ) return ;
  if ( b == null ) return ;
  // メソッド内の処理をする
}

この場合、objA から objB への問い合わせが非常に多く発生するため、パフォーマンス的には良くない(と想像される)のですが、オブジェクト指向の視点から言えば、これが正しいのです。なお、オブジェクト指向でのメソッド呼び出しのコストは「0」となっています。なので、呼び出しが多くても少なくてもスピードは関係ありません(ということになっています)。

この部分、責務を明確にする、ということで、objA で判別するべきか、objB で判別するべきか、というので見解が分かれそうですよね。例えば、次のコードを見てください。

if ( a == "masuda") {
  objB.Method( a, b );
}
// objB の中でパラメータチェックをする
void objB.Method( string a, string b ) {
  if ( a == null ) return ;
  if ( b == null ) return ;
  // メソッド内の処理をする
}

こうなっていた場合、a == “masuda” は何を意味するのでしょうか?ここだけ見ると、変数 a の内容が “masuda” の時に、objB.Method を呼び出す、という条件にしかならないので、これがオブジェクトの内部を考慮してのものなのか、オブジェクトを呼び出すときの条件なのかが判断がつきません。なので、他のところで、

if ( a == "tomoaki" ) {
  objB.Method( a, b );
}

と違う値で呼び出していることを確認した時に、オブジェクトの呼び出し側がつけている条件(呼び出されているクラスの前提条件ではない)ということが判断できます。
なので、書き方として、

・呼び出し側で判断する制御文
・隠蔽化を強化するための制御文

の2つを区別しないといけません。

objB.Method( string a, string b ) {
	// 前提条件
	if ( a != null ) {
	if ( b != null ) {
	// 処理内容
	if ( a == "masuda" ) {
		objC.Method( a );	
	}
	}}
	return ;
}

のように書き分ける必要があるのですね。
たしか、アスペクト指向の書き方として、そんなのがあった覚えがあります。


obj.Method.Before( string a, string b ) {
	if ( a == null ) return ;
	if ( b == null ) return ;
}
obj.Method( string a, string b ) {
	// 処理内容
	if ( a == "masuda" ) {
		objC.Method(a);
	}
	return ;
}
obj.Method.After( string a, string b ) {
	...
}

Method.Before では、本体 method の前処理を担当して、主にパラメータをチェックします。
Method.After では、後処理を担当します。

…と書いたところで、力尽きました orz もうちょっと大雑把に議論したほうがいいみたい。

カテゴリー: 開発 | 純粋オブジェクト指向言語を思考実験してみるテスト はコメントを受け付けていません

デスマーチを脱するための5つの方法(メモ)

後で、パワーポイントに直しますが、メモ的に。
プロジェクトが「デスマーチ」状態に陥ったときには、どうするのか?という5つの方法です。
# 「5」という数にはさして意味がありませんが、まぁ、ちょっとだけは意味があるかも。

思考の元ネタとしては、制約の理論(TOC)と、自己組織化、逃走論、デマルコ著スラック、ピープルウェアあたりです、

・トップマネージャを変える。
→ トップの思惑だけで動いている場合は、トップを変える。
→ あるは、トップの行動/行為がボトルネックになっている。
→ トップがマイナス生産するとき。

・権限を分散する。
→ 権限の抱え込みより、ボトルネックになっているとき。
→ 実行できる権限を分散することで、ボトルネックにならないようにする。
→ 権限を委譲する。

・外部の実行部隊を利用する。
→ ルールそのものがネックになっている場合は、ルールを変える。
→ 内部的な要因であれば、外部のものを使う

・自己組織化する。
→ トップに依存せずに、現場レベルで自己組織化する。
→ ただし、全体最適化は期待できない。

・逃げる
→ 崩壊する場合は、現場の人間としては逃げることが可能。
→ プロジェクトに命を掛けるほどなのかを再考する。

カテゴリー: プロジェクト管理 | デスマーチを脱するための5つの方法(メモ) はコメントを受け付けていません

サプライチェーンから考えるプロジェクトの工期

制約の理論(TOC)の考えを元に、ソフトウェアのプロジェクトの計画を見直します。
基本的なところはゴールドラット氏の理論と一緒で、ソフトウェア特有の「事情」も組み入れます。

と言いますか、この手の確率の計算をシミュレーション(モンテカルロ法)することも多いのですが、条件を簡単にしてモデル化してやれば、計算が可能です。まぁ、モデルの結果を現実にあわせてパラメータを調節する、というやり方なんですけどね。

■二項分布を使って解く

二項分布、正規分布を使って読み解きます。本当は、正規分布の分散を使ったほうが正確に表せるのですが、そもそもが工場の生産現場と違って、ソフトウェアは人が作るところが多いので、物理現象としての正規分布に則らないことが多いのです(学生症候群とか)。なので、あまり正確にやっても意味がないので、現象だけを使うために、単純な二項分布を使います。

■最初の問題

さて、最初の問題は、
 「1日か2日で終わるタスクが10個あるとき、確率50%で終わる日数は、いつだろうか?」
です。
タスクは、1日で終わる確率が50%、2日間掛かってしまう確率が50%としましょう。

さて、この10個のタスクが全て終わった後、50%の確率で終わる日数はいくつでしょうか?

問題の考え方を示すと、

・1つのタスクの最短が1日なので、最短が10日間
・1つのタスクの最長が2日なので、最長が20日間

というわけで、10日から20日の間であることは簡単に分かります。
この間になるわけですが、一体、どの位でしょうか?

Excel で計算したのが次の図です。

▼二項分布で計算

50%ラインが15日間、80%ラインが16日間です。

# 実は、もっと直感的に計算ができて、10~20日の間ということが分かったので、1日と2日の割合が50%ずつだから、15日かなぁ、という概算でもokです。

■次の問題

今度は、もう少し現実にあわせていきましょう。
ひとつのタスクが終了する確率を3つに分けます。

・1日で終わる確率が25%
・2日で終わる確率が50%
・3日で終わる確率が25%

こんな風に、タスクが早く終わるかもしれないし、遅くなるかもしれないし。まぁ、予想通りの場合もあるし、という3パターンの計算です。

このときに、先の問題と同様に50%で終了する期日を求めてみます。

▼三項の確率で計算

Excel を使って計算すると、50%確率の場合は20日間です。
これはすぐに予想が付きますね。タスクが早くなったり遅くなったりするので、前後するとすれば、中央の値が全体の期間に反映されるはずです。なので、ほとんどのタスクが2日間で終わります。
これから概算すると、同じ条件で半分のプロジェクトは、2日間×10タスク=20日間で終わります。

さて、80% の合格ラインを考えると、この図では 22 日間になります。

なので、プロジェクトマネージャは、10% のリスクを上乗せすれば(期間に関しては)80% 安全、と言えます…が、本当でしょうか?

そうなんです、残念ながら、こんな風に10%のリスクでは到底無理、ということを体感的に知っています。
これが、どうしてなのか?を理論/確率を使って説明します。

■学生症候群

先のモデルでは、タスクが早く終わったら次のタスクにすぐに着手する、というルールになっていますが、現実ではそんなことにはなりません。

・次のタスクが始まるまで、暇な時間ができる。
・終わっているタスクを、期日まで延ばそうとする。
・終わっているタスクを、丁寧に仕上げようとする。

ことによって、1日で終わったタスクであっても、結果的に2日間になることがほとんどです。
これを「学生症候群」と言ったりするのですが、製造業の生産の現場では、仕掛品と生産能力の関係であったり、サプライチェーンの考え方であったりします。このあたりは、後日。

なのでモデルを現実にあわせて、1日で終わっても2日間に伸ばしてしまう確率に変えます。

・2日で終わる確率が75%
・3日で終わる確率が25%

のように、1日で終わる確率と2日で終わる確率加えて75%で計算しなおします。

▼学生症候群を含める

こうすると、目標値である20日間の確率がぐっと減り、5%になります。ええ、ほとんど期日通りに終わらせるのは不可能ですね。50% のラインを見ていくと、22 日間、80% のラインを見ていくと、24日間になります。

つまりは、50% のラインでも 10% のリスクを見なければならず、成功させようと思えば、20% の期間の上乗せが必要になってきます。

■学生症候群の確率を変える

先の例では、2日間で終わる確率が75%もありましたが、これを変更します。

・2日で終わる確率が50%
・3日で終わる確率が50%

というモデルを作ります。

▼学生症候群の確率を変える

これを計算すると、50% のラインが 23日間、80% のラインが 23日間になります。
80% のラインが同じように見えますが、92% から 86% にやや下がっています。

この3つの結果を累積のグラフにしたのが下の図です。

▼累積グラフ

80% のラインが大きく右にずれている(期日が延びている)ことが分かりますね。

こんな感じにして、タスクの消化速度と確率を計算していくと、感覚的にどのくらいプロジェクトが伸びそうかが分かってきます。また、確率として計算すると、細かい進捗管理(特に実績の抽出)をしなくても、プロジェクトが期日に終わりそうかどうかが分かります。

勿論、別途リスク管理(プロジェクトを止めてしまうストッパーなので)は必要ですが。
ちょっと、理論のほうは後日アップするとして、ひとまず、確率を使ったプロジェクトの期間見積などを。

カテゴリー: 開発, プロジェクト管理 | 2件のコメント

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

フェイルセーフというのは、機器故障しても安全なほうに倒れるシステムの組み方で、原発の制御棒(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件のコメント

コードを短くすると単体テストが楽になる、の検証編

さて、リファクリング前とリファクタリング後のソースコードがありますが、これがどの程度「安全」にコーディングができるのかを検証していきます。
検証するためには、何らかの定量的な手法が必要です。
# ちなみに「定性的な手法」は「ああ、コードが短くなったね」という感想が根拠になりますw

コードの複雑度を測定するのに、if 文などの制御文や使われている変数の数を勘定する方法がありますが、ここではもうちょっと簡単に、「変更できる箇所」を数え上げていきます。変更できる箇所というのは、コーディング時に間違えやすいところ/間違える可能性があるところで、≒テストが必要なところ、と言えます。

具体的には

・制御文でカウント
・比較文でカウント
・変数の宣言でカウント
・メソッド呼び出しでカウント
・メソッドの変数でカウント

のように、ちまちまと数えていきます。

で、数え上げたのがこれ。

▼リファクタリング前

▼リファクタリング後

赤い丸の箇所が、変更可能な箇所、いわゆる「自由度」になります。

数を勘定すると、
リファクタリング前は、59箇所
リファクタリング後は、36箇所

ざっと言うと、このコードだと 36/59 の差分で 40% 程度改善されている、といえます。

ちなみに、行数を更に短くした src.Select を使ったパターンを見てみると

▼過剰なリファクタリング?

この赤丸は31箇所あります。リファクタリング後のコードとほぼ同じですね。
一見、行数が短くて簡単になっているように思えますが、自由度としてはほぼ同じなので、どちらも使っても同じ不具合の発生率となる、と言えます。

こんな風に自由度をカウントしていくと、コードの複雑度が定量的に計算できるので、コードの複雑度を低くするようにコーディングをしていけば、不具合の発生率が下がって、作業時間が減り、よってプロジェクトの成功確率は上がる、という流れになるんですけどね。
このあたりは、感覚で勘定しても良いので、数万行のコードの中から複雑すぎるメソッドを抽出する、というようなツールができるといいかなぁとか。

カテゴリー: 開発, 設計 | コードを短くすると単体テストが楽になる、の検証編 はコメントを受け付けていません

コードを短くすると単体テストが楽になる、の再実装編

前回の記事
コードを短くすると単体テストが楽になる、の実装編 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2160

で実装上の間違いが発覚したので、やり直し。

■根本的に何をやりたいかを考え直す

のところで、全ての列をコピーするために dest.Rows.Add( r ) なんてことをしていましたが、この変数 r は既に src のテーブルで使っているので Add できないんですよねぇ…というバグが。
ちょっと、動作確認(というか xUnit)していないのがバレバレです。

そんな訳で、

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows )
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		DataRow row = dest.NewRow();
		foreach ( DataColumn col in src.Columns ) {
			string name = col.ColumnName;
			row[ name ] = r[ name ];
		}
		dest.Rows.Add( row );
	}
}

のコードが冗長なので、

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows )
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		dest.Rows.Add( r );
	}
}

のように「全ての列をコピーしたい」というところから再スタートします。

■根本的に何をやりたいのかを考える。

さて、

		DataRow row = dest.NewRow();
		foreach ( DataColumn col in src.Columns ) {
			string name = col.ColumnName;
			row[ name ] = r[ name ];
		}
		dest.Rows.Add( row );

のところは、全ての列の値をコピーしているところなので、かなり冗長です。
やりたいことは、コピーをとる(あるいは、検索した列だけ抽出したい)ということなので、この部分の意図としては、次のコードなのです。

		dest.Rows.Add( r );

ですが、これを動かそうとすると動きません。実装上の制約ですね。
だからといって、元の冗長な形に残しておくのは適切ではありません。

コードを短くする意図としては「何をしたいか明確にする」ということも含まれているので、そのままのコードでは何をしているのか明確ではないのですよね。

■分かり易い名前でメソッドを作る

コピーしていることが分かるように、メソッドで括り出します。
これは、汎用的にする意味ではなく(結果的に汎用的になるでしょうけど)、意図したことを短く記述するという基準からメソッドを作ります。

private void DataRowCopy( DataRow src, DataRow dest ) 
{
	foreach ( DataColumn col in src.Columns ) {
		string name = col.ColumnName;
		dest[ name ] = src[ name ];
	}
}
...

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows )
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		DataRow row = dest.NewRow();
		DataRowCopy( r, row );
		dest.Rows.Add( row );
	}
}

初手としては、DataRow のコピーだけを対象にしておきます。
これでも十分なのですが、前後の dest.NewRow() や dest.Rows.Add( row ) がある文だけ目的のコードよりも長くなっています。

これを目的のコードに近づけるために、DataRowCopy を改良します。

private void AddClone( DataTable dt, DataRow src ) 
{
	DataRow dest = dt.NewRow();
	foreach ( DataColumn col in src.Columns ) {
		string name = col.ColumnName;
		dest[ name ] = src[ name ];
	}
	dt.Rows.Add( dest );
}
...

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows )
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		AddClone( dest, r );
	}
}

AddClone というメソッドを作成して、dest.NewRow() などのメインへの記述がなくなるようにします。

		dest.Rows.Add( r );

と似たような記述になったと思います。

■その方法が適切かどうかを考え直す

ここまで DataTable を使ってきましたが、実は DataTable の機能は使っていないのでリストに直すことができます。そうすると、先に AddClone メソッドを作って DataRow のコピーを行いましたが、これがいらなくなります。

DataTable src ; // 他で設定済み
List<DataRow> dest = new List<DataRow>();
foreach ( DataRow r in src.Rows )
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		dest.Add( r );
	}
}

と、ここまで来て最終的なコードは前回と同じです。

// 指定年齢以上のカラムを取り出す
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 ;
}

さあ、次はこのコードがどの位、複雑度が減っているか検証していきます。

カテゴリー: 開発, 設計 | コードを短くすると単体テストが楽になる、の再実装編 はコメントを受け付けていません

コードを短くすると単体テストが楽になる、の実装編

前回は理論的なところを説明したのですが、では具体的どうするのか?ということで。

コードを短くすると単体テストが楽になる、の証明 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2157

最初に、こんなコードがあるとします。

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
for ( int i=0; i<src.Rows.Count; i++ ) 
{
	if ( (int)src.Rows[i].Item("age") >= 20 ) {
		DataRow row = dest.NewRow();
		row["id"] = src.Rows[i].Item("id");
		row["age"] = src.Rows[i].Item("age");
		row["name"] = src.Rows[i].Item("name");
		row["address"] = src.Rows[i].Item("address");
		row["telephone"] = src.Rows[i].Item("telephone");
		dest.Rows.Add( row );
	}
}

何をやっているかというと、元のデータテーブル(src)から年齢(age)が20才以上の人を、別のデータテーブル(dest)にコピーしています。
分かり易いといえば分かり易いような(事実、これを分かり易いと主張する方も多いので)、分かりにくいと言えば分かりにくいというような微妙なコードです。

なので、個人的な視点から「わかりやすい」か「わかりにくい」かというのは生産的ではないので、業務の視点や保守の視点から、このコードを変えていきます。
コードをうまく整理すると、コード上の情報が消えて(見えなくなって)、結果的にはコードの質が上がります。

■カウンター変数をやめる

最初は、変数 i で使われているカウンターをやめます。カウンターは配列やリストなどをアクセスするときに便利な手段なのですが、現在 foreach などのループ専用の構文があることを考えると「遺物」と言えます。

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows ) 
{
	if ( (int)r.Item("age") >= 20 ) {
		DataRow row = dest.NewRow();
		row["id"] = r.Item("id");
		row["age"] = r.Item("age");
		row["name"] = r.Item("name");
		row["address"] = r.Item("address");
		row["telephone"] = r.Item("telephone");
		dest.Rows.Add( row );
	}
}

変数 i は、Rows コレクションの添え字でしか使われていません。よって、foreach 文で書き換えることができます。これによって、「変数 i が途中で変更される恐れ」が無くなります。また、foreach 文の中では、変数 i による間接的な参照ではなく、変数 r による直接的な参照に切り替えられます。
これは、俗に言うセキュリティ上の変数の「汚染」を防ぐことができます。

■r.Item(“age”) を一時変数に代入する

一時変数の使い方がうまくないコードがあります。

	if ( (int)r.Item("age") >= 20 ) {

この部分は、一見、int にキャストしていて、行数が短くコンパクトになっているように見えますが、保守を考えると、次のように書くほうがよいでしょう。

	int age = (int)r.Item("age");
	if ( age >= 20 ) {

1行のコードが2行になってしまい、冗長なコードに見えますが、実は実行されるときの挙動が異なります。

r.Item(“age”) の中身が int ではない場合、int のキャストに失敗するので、例外は int age = (int)r.Item(“age”); の時点で発生します。元のコードでは if 文内で発生します。
この場合、デバッグ実行をしなければいけないときに、上記のように一時変数に代入しておくと、デバッグがしやすくなります。例えば、どうも 20歳以上とは思えないデータを検出したとき、いわゆる print デバッグ(Console.WriteLine によるデバッグ)をする場合、このように書けます。

	int age = (int)r.Item("age");
	Console.WriteLine("age: {0}", age );
	if ( age >= 20 ) {

ところが、if 文の中で直接キャストしてしまうと、次のように冗長になるし、先の int のキャストを含めるとデバッグ自体をややこしくしてしまいます。

	Console.WriteLine("age: {0}", (int)r.Item("age") );
	if ( (int)r.Item("age") >= 20 ) {

# 実際は、WriteLine メソッドは、object 型の引数を取るので int のキャストを削っても良いのですが、ここでは説明のためにつけています。

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows ) 
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		DataRow row = dest.NewRow();
		row["id"] = r.Item("id");
		row["age"] = r.Item("age");
		row["name"] = r.Item("name");
		row["address"] = r.Item("address");
		row["telephone"] = r.Item("telephone");
		dest.Rows.Add( row );
	}
}

■同じ情報を二重に書かないようにする

更に問題なのは、id, age, name, address, telephone などの特定な文字列(マジックナンバー)が含まれていることです。この情報は一体何でしょうか?
おそらく、元のテーブルからコピーしているのですが、本当にそうなのか定かではありません。というのも
id という文字列が2回出てきてしまっているために、

・全て id, age, name… という列名をコピーしたいのか?
・変換しながら id などの列名をコピーしたいのか?

という2つの情報が交じり合っています。
なので、全ての列名をそのままにしてコピーしたい、ということを強調するために次のように書き換えます。

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows ) 
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		DataRow row = dest.NewRow();
		string[] names = {
			"id", "age", "name", "address", "telephone" };
		foreach ( string name in names ) {
			row[ name ] = r[ name ];
		}
		dest.Rows.Add( row );
	}
}

こんな風に列名を使った配列を定義して、コピーをします。こうすることで「同じ列名」をコピーすることを強調します。逆に言えば、同じ列名ではない場合があるときは、先のように明示的に列名を書くわけです。

ただし、この場合でも2つの情報が混在しています。
・一部の列をコピーしたいのか?
・全ての列をコピーしたいのか?

一部の場合は、この例でも良いのですが、すべての列の場合はもう一工夫しましょう。列名がコードに現れないようにして情報を減らします。

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows ) 
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		DataRow row = dest.NewRow();
		foreach ( DataColumn col in src.Columns ) {
			string name = col.ColumnName;
			row[ name ] = r[ name ];
		}
		dest.Rows.Add( row );
	}
}

こうすると「全ての列をコピーする」という情報が強調され、「一部をコピーする」という情報が消え去ります。

■根本的に何をやりたいかを考え直す

さて、コードから情報を減らすという方法でコードを変えてきました。こうすると、このコードの目的がはっきりしてきます。

・年齢(age)が20以上のカラムを抽出する。
・そのカラムを別のテーブルにコピーする。

とう正確なコードに対する要件です。これの要件は、最初のコードでも満たせていたのかもしれませんが、コードを短くする中で明確になってきています。

そのカラムを別のテーブルにコピーする、という段階で2つの条件が混在しています。

・コピーしたカラムは、修正前と修正後のデータを残さないといけない。
・コピーしたカラムは、修正後のデータだけ残っていればよい。

最初のコードでは、コピーをしているだけなので、この意図が判然としません。
逆に言えば、修正前のデータは残さなくてもよい(あるいは、修正前のデータに反映する必要がある)ときには、次のように元のデータを直接、参照先のテーブルに入れることができます。

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows ) 
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		dest.Rows.Add( r );
	}
}

一気にコードが短くなりました。コピー元の情報を取っておく場合には、NewRow メソッドで新しい行を作る必要があるのですが、条件にマッチしたデータだけがほしい場合には、書き方を一変させることで、更に情報を削ることができます。

■その方法が適切かどうかを考え直す。

ここまでは、コピー先も DataTable で用意することにしていましたが、果たして DataTable である必要があるのでしょうか?
条件にマッチした行が必要ならば、List を使ってもよいわけです。
ここでコレクションを利用するのは、DataTable まで高機能ではなくてもよい、というメモリ的な視点からでています。

DataTable src ; // 他で設定済み
List<DataRow> dest = new List<DataRow>();
foreach ( DataRow r in src.Rows ) 
{
	int age = (int)r.Item("age");
	if ( age >= 20 ) {
		dest.Add( r );
	}
}

■機能に適切な名前をつける

ここから、リファクタリングの分野になります。

・年齢(age)が20以上のカラムを抽出する。
・そのカラムを抽出する

という条件が明確になるようにメソッドとして切り出します。

// 指定年齢以上のカラムを取り出す
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 ;
}

これで呼び出し側では、たったの1行で済むことになります。
しかも、

・年齢(age)が20以上のカラムを抽出する。
・そのカラムを抽出する

という条件が非常に明確になります。

■メソッド内をリファクタリングする

メソッド内のコードは、xUnit を使えば自由に書き換えることができるので、便利なライブラリがあればそれを使えばよいでしょう。高速化やメモリ効率化などは、この段階で行います。
DataTable の Select メソッドを使うと、非常に小さく書けます。

// 指定年齢以上のカラムを取り出す
List<DataRow> dest = SelectMoreAge( src, 20 )

// 指定年齢以上のカラムを取り出すメソッド
List<DataRow> SelectMoreAge( DataTable src, int age )
{
	List<DataRow> dest = new List<DataRow>();
	dest.AddRange( src.Select(string.Format("age >= {0}", age )));
	return dest ;
}

切り出したメソッドが多少読みにくくなってしまっていますが、ここは xUnut で十分テストできる箇所なので、多少複雑なっても保守性に問題はありません。
分かりづらい場合は、先のメソッドの状態で残しておいても良いと思います。

■どちらのコードが保守性が高いか?

さて、あらためて最初のコードと見比べてみましょう。

変更前がこちら

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
for ( int i=0; i<src.Rows.Count; i++ ) 
{
	if ( (int)src.Rows[i].Item("age") >= 20 ) {
		DataRow row = dest.NewRow();
		row["id"] = src.Rows[i].Item("id");
		row["age"] = src.Rows[i].Item("age");
		row["name"] = src.Rows[i].Item("name");
		row["address"] = src.Rows[i].Item("address");
		row["telephone"] = src.Rows[i].Item("telephone");
		dest.Rows.Add( row );
	}
}

変更後は1行になります。

// 指定年齢以上のカラムを取り出す
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 ;
}

メソッドで括りだしているところが「ずるい」ように見えますが、実は業務で使うコードなのにメソッドで括りだしていないことが「危ない」と私は思っています。
ただし、作業量的に(時間的な制約が大きいので)ここまで考慮することはできないことが多いのですが、多少なりとも業務的なコードであれば、

・年齢(age)が20以上のカラムを抽出する。
・そのカラムを抽出する

という要件に絞り込んで、さっくりとメソッドで括りだすことぐらいはしないとね > 自分、といったところで(苦笑)

カテゴリー: 開発, 設計 | 2件のコメント