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

制約の理論(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(&quot;age&quot;);
		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(&quot;age&quot;);
			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(&quot;age&quot;);
				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(&quot;age&quot;);
		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(&quot;age&quot;);
		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(&quot;age&quot;);
	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(&quot;age&quot;);
	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(&quot;age&quot;);
	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(&quot;age&quot;);
	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(&quot;age&quot;);
	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(&quot;age&quot;);
		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(&quot;age&quot;) >= 20 ) {
		DataRow row = dest.NewRow();
		row[&quot;id&quot;] = src.Rows[i].Item(&quot;id&quot;);
		row[&quot;age&quot;] = src.Rows[i].Item(&quot;age&quot;);
		row[&quot;name&quot;] = src.Rows[i].Item(&quot;name&quot;);
		row[&quot;address&quot;] = src.Rows[i].Item(&quot;address&quot;);
		row[&quot;telephone&quot;] = src.Rows[i].Item(&quot;telephone&quot;);
		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(&quot;age&quot;) >= 20 ) {
		DataRow row = dest.NewRow();
		row[&quot;id&quot;] = r.Item(&quot;id&quot;);
		row[&quot;age&quot;] = r.Item(&quot;age&quot;);
		row[&quot;name&quot;] = r.Item(&quot;name&quot;);
		row[&quot;address&quot;] = r.Item(&quot;address&quot;);
		row[&quot;telephone&quot;] = r.Item(&quot;telephone&quot;);
		dest.Rows.Add( row );
	}
}

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

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

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

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

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

	int age = (int)r.Item(&quot;age&quot;);
	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(&quot;age&quot;);
	Console.WriteLine(&quot;age: {0}&quot;, age );
	if ( age >= 20 ) {

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

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

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

DataTable src ; // 他で設定済み
DataTable dest = new DataTable();
foreach ( DataRow r in src.Rows ) 
{
	int age = (int)r.Item(&quot;age&quot;);
	if ( age >= 20 ) {
		DataRow row = dest.NewRow();
		row[&quot;id&quot;] = r.Item(&quot;id&quot;);
		row[&quot;age&quot;] = r.Item(&quot;age&quot;);
		row[&quot;name&quot;] = r.Item(&quot;name&quot;);
		row[&quot;address&quot;] = r.Item(&quot;address&quot;);
		row[&quot;telephone&quot;] = r.Item(&quot;telephone&quot;);
		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(&quot;age&quot;);
	if ( age >= 20 ) {
		DataRow row = dest.NewRow();
		string[] names = {
			&quot;id&quot;, &quot;age&quot;, &quot;name&quot;, &quot;address&quot;, &quot;telephone&quot; };
		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(&quot;age&quot;);
	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(&quot;age&quot;);
	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(&quot;age&quot;);
	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(&quot;age&quot;);
		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(&quot;age >= {0}&quot;, 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(&quot;age&quot;) >= 20 ) {
		DataRow row = dest.NewRow();
		row[&quot;id&quot;] = src.Rows[i].Item(&quot;id&quot;);
		row[&quot;age&quot;] = src.Rows[i].Item(&quot;age&quot;);
		row[&quot;name&quot;] = src.Rows[i].Item(&quot;name&quot;);
		row[&quot;address&quot;] = src.Rows[i].Item(&quot;address&quot;);
		row[&quot;telephone&quot;] = src.Rows[i].Item(&quot;telephone&quot;);
		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(&quot;age&quot;);
		if ( a >= age ) {
			dest.Add( r );
		}
	}
	return dest ;
}

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

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

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

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

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

感覚的には、コードを短くすると、単体テストが楽になり、結果的に品質があがり、最終的にはプロジェクトの成功率が上がる、ことは理解っているのだけど、人に説明すると、どうも「納得いか~ん」方が居られるので、ちょっと論理的に証明などしてみます。

# 実は、証明するよりも、「人は理解したいものを、理解する」という訳で、ロジックで説明しても無駄なことが多いんですけどね。ま、自分を納得させるためにも必要ということで。

■現状分析

ソフトウェア開発において、ステップ数でプロジェクト規模換算をしようとすると、請負側からすれば規模を大きく見せる≒プロジェクトを擬似的に大きく見せる、ためにステップ数を「実効数」よりも増やすという現象があります。
また、プロジェクトが終わった段階で、ステップ数が多いということが、イコール「たくさん仕事をした」ということで、報酬が多くなるという現象もあるわけです。

なので、ステップ数を減らすと、「仕事量が少ない」ゆえに「報酬が少なくなる」ということに成りかねないので、この話は、

・自社で、パッケージアプリケーションを開発する。
・プロジェクトの成否が、自社の報酬に影響する。

時に使ってください。

# F# などの関数言語を使ってみると、短く書く≒簡素に書くことの重要性がわかるのですが、全てのプログラマが関数言語を使ってみるというのも難しい話なので。

■背理法で証明する

厳密に言えば、最初は詭弁的な方法で論証してみます(笑)。手っ取り早いので。

1.ステップ数が多いコードがある。
2.ステップ単位で予想されるバグ件数は決まっている(品質上)
3.ゆえに、バグ件数は、ステップ数の比例する。

バグ件数 = ステップ数 × 予想バグ比率

ということで、ステップ数を増えれば、バグ件数が増える。
なので、バグ件数を減らすためには、ステップ数を減らせばよい。

証明おわり。

…っての考えたのですが、論理的に間違っているので「証明」にはなりません。「説得」にはなるけど。

weak point は、
・予想バグ比率は、一定とは限らない。
 → 例えば、自動生成されるコードは、バグを一切含まない。
・予想バグ比率は、実際は人依存である。
 → 例えば、ベテランと新人では、予想バグ比率が異なる。
・バグ件数が、そのままプロジェクトの負荷になるわけではない。
 → 例えば、細かいバグと重大バグでは、1件の重みが異なる。
 → システム試験などで見つかる(あるいは見つからない)潜在バグのほうが、プロジェクトの負荷となる。
のように続々とでてきます。

まぁ、これらの反論が、コード数を減らそうとはしない、という人の動きになるのですが。

経済心理学的に、ステップ数を減らすと短期的にコストが減る、という幻想を生み出しておくのと、現実にプロジェクトのコストが減る、という現実を作らないと駄目なのです。

■in/out を明確にして DFD 風に証明する。

ソフトウェア開発のプロジェクトの肝は、情報量を如何にして制御するか、です。
「情報量」ってのは、

・要件定義の工程で確定した契約 だったり
・アプリケーションの機能数 だったり
・Web アプリケーションのページ数 だったり

するわけですが、まぁ、一般的に「作る量」ってやつです。

# 「情報量」については、後に正確に定義しないと駄目だなぁと思っています。
# DFD を使うか、IDEF0 を使うか、そのあたりを制御しないと PERT 図に落とせないので。

ソフトウェア開発での作業量というのは、以下の法則があります。

・人が作成する量があれば、単純に時間がかかる。
 → 単純に人件費が、という話です。
 → なので自動化されるもの(自動化されたドキュメント、自動生成されたコード)は、作業量に含みません。
・人が作成するものが複雑であれば、単純に時間がかかる。
 → 同様に、頭を使う時間が必要ということです。
 → 複雑怪奇なコードでも、自動生成されたり、ライブラリでモジュール化されているものは、人に対しては「複雑」とは言いませんね。
・人とのコミュニケーションが発生すれば、時間がかかる。
 → 顧客との問い合わせ、遠隔地での電話会議、ひとりではなく複数名で作る場合などです。
 → いわゆるコミュニケーション・コストです。

さて、これを前提にすると、

コードの量が少ない場合、その状態として次が考えられます。

・コードをタイピングする時間が少ない
 → IDE などでインテリセンスを利用するとタイピングが減りますが、先の条件で「自動化」の部分にあてはまります。
 → コピー&ペーストも、タイピングを少なくする手段のひとつです。
・コードを簡素にする
 → 複雑なものよりも、簡単なもののほうが、時間が少なく書ける。
・ひとつのコードを、ひとりが扱う
 → ひとつのコードを複数名でやり取りするよりも、ひとりで完成させたほうがコミュニケーションコストは下がります。

これを因果関係で直すと、

1.コードのタイピングを減らす → 作業量が減る。
2.コードを簡素にする → 作業量が減る。
3.コードをひとりが扱う → 作業量が減る。

のいずれかのパターンになります。
つまりは、いずれかのパターンを取ると「作業量が減る」という結果が得られます。

# ちなみ言うと、「コードのタイピングを増やす」→「作業量が減る」という論理も正しいのです。
# こちらのほうは証明しません。命題は「確実に作業量が減る方法は?」なので。

この中で、1.のコードのタイピングを減らす方法で作業量を考えると、

1.コードのタイピングを減らす → 作業量が減る。
2.作業量が減る → コード量が減る。
3.コード量が減る → バグ混入率が同じであれば、バグの発生数が減る。
4.バグの発生数が減る → バグの難易度が同じであれば、不具合を直す総時間は減る。
5.不具合を直す時間が減る → アプリケーションの完成が早くなる

ゆえに、ひとつのアプリケーションを扱ったときは、コードのタイピングを減らせば、バグの発生数が減り、アプリケーションが早く完成するということになります。

ただし、これには仮説が入っています。

・バグの混入率が同じであること。
・バグの難易度が同じであること。

ですね。これはどうなのでしょうか?

例えば、perl のようなワンライナーの場合は、バグの混入率は違いますよね。また、難易度も違います。
このバグの混入率、難易度を「同じ」にする前提としては、

・コードが短くなっても、1行のコードの複雑度は同じ。

というケースが考えられます。
具体的に言えば、

・1行で扱う変数の数は、変わらない。
・1行で扱うメソッドの数は、変わらない(メソッドチェーンは、理解度を上げるが、複雑度を上げる)。

という指標があります。

なので、結論で言えば、

1行で扱う変数やメソッド数を変えずに、複雑度が上がらない状態で、
コード数を減らすと、アプリケーションが早く完成する。

ゆえに、プロジェクトの成功率が上がる。

という論法です。

さて、実務はどうなるかというのは後ほど。

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

新規ソフトウェア開発においてのプランニング

通勤中にぽちぽちとまとめた結果をアップしておきます。

・要素を抜き出しているだけなので、別途つながりを検討する必要があり。
・一般的なウォーターフォール開発の流れで。
→ ただし、顧客の要望を取り込めるように、顧客からのフィードバックを用意する。
→ アジャイル開発の場合は別途作成する(予定…予定は未定)

>> 要件定義
 >> 顧客要望まとめ
  >> 業務分析
  >> 機能要件
  >> 性能要件
  >> 顧客スケジュール
 >> システム概要設計
 >> 見積もり
  >> 全体スケジュール
  >> 期間見積
  >> 配員計画
  >> 金額見積
>> 見積提出
 >> 交渉
>> プロジェクト計画
>> 設計工程
 >> システム設計
  >> オブジェクト指向設計
  >> データ構造設計
  >> ネットワーク構造設計
  >> ユーザーインターフェース設計
   >> 画面設計
   >> 顧客レビュー
>> 実装工程
 >> 詳細設計、内部設計
 >> 実装
 >> 単体試験
 >> 結合試験
>> 試験工程
 >> 試験計画
  >> 不具合票の取り回し
 >> システム試験
  >> 機能試験
  >> 性能試験
   >> 負荷試験
 >> ユーザー試験
  >> 実装へフィードバック
 >> 運用試験
>> システム導入
 >> 導入計画
 >> 導入作業
 >> 導入試験
 >> 運用マニュアル
 >> 試験運用
>> 運用
 >> 初期サポート
 >> 初期障害対処
 >> 運用研修/教育
>> 保守
 >> 限界値チェック
 >> ナレッジベースへの蓄積
 >> 変更要望
 >> 障害対処 

PMBOK の用語に合わせようと思ったけど、PMBOK の場合、実装(programing)が薄すぎるのでやめました。詳細設計から結合試験までを【実装工程】としました。全体のバランスのためです。
パレードの法則から言えば、より金額の掛かっているところ(あるいはリスクの高いところ)にプロジェクトの投資を注力するべきで、その他は現状の流れに任せてもよい、という考えです。お金もスタッフも限りがありますからね。ビジネス的に対費用効果を考える、ということです。

カテゴリー: プロジェクト管理, Plan Language | 新規ソフトウェア開発においてのプランニング はコメントを受け付けていません

Plan Language を実践中

計画言語を計画中でちょっと止まっておりますが(裏側で xUnit で頭を使っていたからというのもあり)、別件のサイトを作成中です。
いつものパターンならば、Excel で ToDo リストを出していくところなのですが、どうせならばと計画言語を使ってタスクを抽出してみました。

本番環境へリリース
<< データ投入
<< Excel から insert 文へ変換スクリプト
<< 画像ファイルアップロード
<< 本番環境での動作確認
<< oreoreMVC のコピー
<< PHP4 の文字コード確認
<< PHP4+MySQL の動作確認
v << 画像のネーミングを決定 -> slug で
<< 画像のフォルダ構成を決定

<< 本番 view を作成
<< 簡易 view を作成
<< controller 作成
v << 医院詳細表示
<< 医院名で検索
<< 医院名で探す
<< 「あ」をクリックしたときの地図
<< エリアで検索
<< エリアで探す画面
<< エリアを選択した後の地図
<< 駅名で検索
<< 駅名で探す画面
<< 駅名を選択した後の地図
<< お口の健診検索
<< すこやかちゃん検索
<< 口腔がん検診検索
v << 単一テーブルに対する model を作成

言語っぽいところは、

・「<<」で、前の WBS をブレークダウン
・インデント(ひとつの空白)でツリーを表現)

しているだけです。コンパイル可能にするためには Task[“…”] なりをつけないと駄目なんですが、これだとメモ帳などでコーディングが可能です…と言いますか、これ、iPad のメモで書きました。
通勤中にぽちぽちと計画が立てられるので便利です。

そこで思い出したのが、その昔 IBM の workpad で擬似プログラミングをしていたんですよね。やっぱりメモ帳を使って、簡略化した記号を使って書いていました。

さて、この plan langauge の要素を取り出してみると、

1.インデントで親子の WBS を表現する。
2.前後の行で時系列(逆時間)を表現する。先頭がトップ/完成すべき WBS です。
3.完了は「v」で表現する。
4.WBS の内容は、「<<」に続いて記述する。

という具合です。
作って&使ってみて気づいたのですが、実は 2 が重要です。単純なツリー構造の場合には、兄弟 WBS の前後関係は現れません。勿論、DOM のようにデータ構造として保持されますが、ブレークダウンする過程で兄弟 WBS の関連は失われてしまうのです。
なので、このテキスト版の plan language は、行の前後関係が情報として重要になります。時系列として下から上に作業をする、ということですね。

これらの情報を明確にしてデータ構造を作ると、次のように書けます。

class WBS {
public WBS parent ;
public List<WBS> childWBS ;
public List<WBS> beforeWBS ;
public List<WBS> nextWBS;
public string task ;
public bool completed;
}

こんな感じで、前後の wbs を識別します。beforeWBS と nextWBS が list になっているのは、PERT 図のように前後のタスクが複数になったときのためです。

まぁ、こうやってタスクを出してから、粛々とコーディングをする、というわけで。
ええ、もちろん、コーディングをする間も先のタスクは増えたり減ったりします。

カテゴリー: プロジェクト管理, Plan Language | 1件のコメント

コードを短く書く努力をするよ(VC++編)

さて、C# や VB でコードを短くするには限界があります。
という具体例を出しましょう。

まずは、先ほどの VB のコードを再掲します。

Public Sub InitStatusBar()
 SETSTATUS(Button1, &quot;先頭のボタンです&quot;)
 SETSTATUS(Button2, &quot;真ん中のボタンです&quot;)
 SETSTATUS(Button3, &quot;一番舌のボタンです&quot;)
 SETSTATUS(ComboBox1, &quot;項目を選択します&quot;)
 SETSTATUS(TextBox1, &quot;名前を入力します&quot;)
 SETSTATUS(TextBox2, &quot;年齢を入力します&quot;)
 SETSTATUS(TextBox3, &quot;住所を入力します&quot;)
End Sub

Private _statusdic As New Dictionary(Of Control, String)
''' <summary>
''' フォーカス時のメッセージを設定
''' </summary>
''' <param name=&quot;c&quot;></param>
''' <param name=&quot;msg&quot;></param>
''' <remarks></remarks>
Public Sub SETSTATUS(ByVal c As Control, ByVal msg As String)
 _statusdic.Add(c, msg)
 AddHandler c.MouseEnter, AddressOf COM_MouseEnter   'MouseEnter
 AddHandler c.MouseLeave, AddressOf COM_MouseLeave   'MoueeLeave
End Sub

Public Sub ChangeFocus(ByVal sender As Object, ByVal b As Boolean)
 If b = False Then
 ToolStripStatusLabel1.Text = &quot;&quot;
 Else
 ToolStripStatusLabel1.Text = _statusdic(CType(sender, Control))
 End If
End Sub

Private Sub COM_MouseEnter(ByVal sender As System.Object, ByVal e As System.EventArgs)
 ChangeFocus(sender, True)
End Sub
Private Sub COM_MouseLeave(ByVal sender As System.Object, ByVal e As System.EventArgs)
 ChangeFocus(sender, False)
End Sub

これでも十分短いですよね。しかも Dictonary を使っているから効率が良さそうです。
ですが、Dictonary を使っているために、イベントがワンクッション遅くなっています。大抵のイベントはこれでいいのですが、高速化が問題になるときは、この【ワンクッション】が致命的になります。

じゃあ、ってんで VB や C# の場合は冗長パターンに書き直す(あるいは、スクリプトを使ってコードを自動生成する)ことになるんですが、C++ を使うとこの両方の条件を満たすことができます。

#define SETEVENT( cn ) \
this->cn->MouseLeave += gcnew System::EventHandler(this, &Form1::cn##_MouseLeave); \
this->cn->MouseEnter += gcnew System::EventHandler(this, &Form1::cn##_MouseEnter);

#define SETSTATUS( cn, msg ) \
private: System::Void cn##_MouseEnter(System::Object^  sender, System::EventArgs^  e) {    \
 this->ToolStripStatusLabel1->Text = msg;    \
} \
private: System::Void cn##_MouseLeave(System::Object^  sender, System::EventArgs^  e) { \
 this->ToolStripStatusLabel1->Text = &quot;&quot;;    \
}

private: System::Void Form1_Load(System::Object^  sender, System::EventArgs^  e) {
 // ステータスバーのイベントを設定
 SETEVENT( Button1 );
 SETEVENT( Button2 );
 SETEVENT( Button3 );
 SETEVENT( ComboBox1 );
 SETEVENT( TextBox1 );
 SETEVENT( TextBox2 );
 SETEVENT( TextBox3 );
}

/// ステータスバーの設定
SETSTATUS( Button1, &quot;先頭のボタンです&quot;);
SETSTATUS( Button2, &quot;真ん中のボタンです&quot;);
SETSTATUS( Button3, &quot;一番下のボタンです&quot;);
SETSTATUS( ComboBox1 , &quot;項目を選択します&quot;);
SETSTATUS( TextBox1, &quot;名前を入力します&quot;);
SETSTATUS( TextBox2, &quot;年齢を入力します&quot;);
SETSTATUS( TextBox3, &quot;住所を入力します&quot;);

既にマジックなコードになっていますが、見かけ上は分かりやすいですよね。
保守という点では、SETEVENT と SETSTATUS でマクロが2つに分かれていますが、先の VB のコードのように

・一時的な List や Dictonary を使わない(メモリ効率)
・イベント呼び出しにワンクッション置かない(高速化)

という条件が満たされています。

ここでトリッキーな使い方をしているのが #define のところで、ボタン名などの名前からイベント名を作成しています。イベント名を個別に作成するので、見掛け上は 1 行のコードですが、実際は複数行書かれています。
C/C++ の場合は、割とこのようなコードを書くのが通常のパターンなのですが、残念ながら VB や C# の場合は、このパターンが使えないんですよね。

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

コードを短く書く努力をするよ(2)

コードを短く書く努力をすると、テストが省ける | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2135/

の続きです。上記では refactoring 前を書いていないので、メリットが良く分からない。ということで、冗長な書き方をさらしておきます。

■ステータスバーを設定(冗長パターン)

ボタンなどのコントロールの、MouseEnter と MouseLeave にちまちま書きます。

    ''' <summary>
    ''' Button1 の MouseEnter
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub Button1_MouseEnter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.MouseEnter
        ' ステータスバーにメッセージを表示
        ToolStripStatusLabel1.Text = &quot;一番最初のボタンです&quot;
    End Sub

    ''' <summary>
    ''' Button1 の MouseLeave
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub Button1_MouseLeave(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.MouseLeave
        ' ステータスバーのメッセージをクリア
        ToolStripStatusLabel1.Text = &quot;&quot;
    End Sub

    ''' <summary>
    ''' Button2 の MouseEnter
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub Button2_MouseEnter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.MouseEnter
        ' ステータスバーにメッセージを表示
        ToolStripStatusLabel1.Text = &quot;真ん中のボタンです&quot;
    End Sub

    ''' <summary>
    ''' Button2 の MouseLeave
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub Button2_MouseLeave(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.MouseLeave
        ' ステータスバーのメッセージをクリア
        ToolStripStatusLabel1.Text = &quot;&quot;
    End Sub

    ''' <summary>
    ''' Button3 の MouseEnter
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub Button3_MouseEnter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.MouseEnter
        ' ステータスバーにメッセージを表示
        ToolStripStatusLabel1.Text = &quot;一番下のボタンです&quot;
    End Sub

    ''' <summary>
    ''' Button3 の MouseLeave
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub Button3_MouseLeave(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.MouseLeave
        ' ステータスバーのメッセージをクリア
        ToolStripStatusLabel1.Text = &quot;&quot;
    End Sub

長い…長すぎますッ!!! Visual Studio を使うとヘッダのコメントは自動生成してくれるのですが、これで 3 つしかボタンに割り当てていません。
コード的には、MouseEnter と MouseLeave のワンセットで 20 行かかります。
なので、10 個のボタンがあれば、200 行掛かるわけですね。

■ステータスバーを設定(イベントを共通化、でも冗長)

これでは、あまりにも、なのでイベントを共通化します。

    ''' <summary>
    ''' Button1 の MouseEnter
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub ButtonCom_MouseEnter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles _
        Button1.MouseEnter, _
        Button2.MouseEnter, _
        Button3.MouseEnter
        ' ステータスバーにメッセージを表示
        Dim msg As String = &quot;&quot;
        If sender Is Button1 Then
            msg = &quot;一番最初のボタンです&quot;
        ElseIf sender Is Button2 Then
            msg = &quot;真ん中のボタンです&quot;
        ElseIf sender Is Button3 Then
            msg = &quot;一番下のボタンです&quot;
        End If
        ToolStripStatusLabel1.Text = msg
    End Sub

    ''' <summary>
    ''' Button1 の MouseLeave
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub ButtonCom_MouseLeave(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles _
        Button1.MouseLeave, _
        Button2.MouseLeave, _
        Button3.MouseLeave
        ' ステータスバーのメッセージをクリア
        ToolStripStatusLabel1.Text = &quot;&quot;
    End Sub

おお、ずいぶんすっきりしましたね。イベントをひとつにまとめたので【共通化】されています。elseif の羅列のかわりに select case を使う方法もあります。

    ''' <summary>
    ''' Button1 の MouseEnter
    ''' </summary>
    ''' <param name=&quot;sender&quot;></param>
    ''' <param name=&quot;e&quot;></param>
    ''' <remarks></remarks>
    Private Sub ButtonCom2_MouseEnter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles _
        Button1.MouseEnter, _
        Button2.MouseEnter, _
        Button3.MouseEnter
        ' ステータスバーにメッセージを表示
        Dim msg As String = &quot;&quot;
        Dim cont As Control = TryCast(sender, Control)
        Select Case cont.Name
            Case &quot;Button1&quot;
                msg = &quot;一番最初のボタンです&quot;
            Case &quot;Button2&quot;
                msg = &quot;真ん中のボタンです&quot;
            Case &quot;Button3&quot;
                msg = &quot;一番下のボタンです&quot;
        End Select
        ToolStripStatusLabel1.Text = msg
    End Sub

さて、日曜プログラマならば、この程度で許せるのですが、【業務プログラマー】ならば、この時点で2点気づかないと駄目です。

・Button1.MouseLeave 等が、二重に記述されている。
・メッセージが埋め込まれている。

イベントが多くなると、Handles を2箇所修正しないと駄目ですね。VB v2.0 特有ではありますが、継続記号「_」を書く必要があるので、結構面倒です。
また、elseif でも select case でも、所詮は【設定】なのですから、配列/リスト/ディクショナリを使ったほうが、【設定】として際立ちます。

■ステータスメッセージとイベントを設定風に変える

これが、前回書いたときのの【意図】です。
設定として記述するので、SETSTATUS のような関数を使って並べていきます。

Public Sub InitStatusBar()
	SETSTATUS(Button1, "先頭のボタンです")
	SETSTATUS(Button2, "真ん中のボタンです")
	SETSTATUS(Button3, "一番舌のボタンです")
	SETSTATUS(ComboBox1, "項目を選択します")
	SETSTATUS(TextBox1, "名前を入力します")
	SETSTATUS(TextBox2, "年齢を入力します")
	SETSTATUS(TextBox3, "住所を入力します")
End Sub

ここのメッセージを enum なり const string なりを参照しても良いですし、外部ファイルから読み込みをしても構いません。

	Class STATMSG
		Public cont As Control
		Public msg As String
	End Class
	Private _status As New List(Of STATMSG)
	''' <summary>
	''' フォーカス時のメッセージを設定
	''' </summary>
	''' <param name=&quot;c&quot;></param>
	''' <param name=&quot;msg&quot;></param>
	''' <remarks></remarks>
	Public Sub SETSTATUS(ByVal c As Control, ByVal msg As String)
        Dim sm As New STATMSG()
        sm.cont = c
        sm.msg = msg
        AddHandler c.MouseEnter, AddressOf COM_MouseEnter   'MouseEnter
        AddHandler c.MouseLeave, AddressOf COM_MouseLeave   'MoueeLeave
		_status.Add(sm)
	End Sub
''' <summary>
''' フォーカス時のメッセージを設定
''' </summary>
Public Sub ChangeFocus(ByVal sender As Object, ByVal b As Boolean)
	If b = False Then
		ToolStripStatusLabel1.Text = &quot;&quot;
	Else
		For Each cm In _status
			If cm.cont Is sender Then
				ToolStripStatusLabel1.Text = cm.msg
			End If
		Next
	End If
End Sub

ここまで共通関数で、以下のイベントを form に追加します

Private Sub COM_MouseEnter(ByVal sender As System.Object, ByVal e As System.EventArgs)
	ChangeFocus(sender, True)
End Sub
Private Sub COM_MouseLeave(ByVal sender As System.Object, ByVal e As System.EventArgs)
	ChangeFocus(sender, False)
End Sub

こうすると、コントロールが増えても InitStatusBar 関数に1行追加するだけで済みます。
また、メッセージをファイル読み込みするときは、InitStatusBar 関数でメッセージをファイルから読み込むように書き換えるだえけでいいのです。

■高速化するためにリファクタリングする

この設定では List を使ったのですが、Dictonary を使ったほうがループがなくなります。これはリファクタリングの範囲なので、インターフェースは変わらないように作ります。

    Private _statusdic As New Dictionary(Of Control, String)
    ''' <summary>
    ''' フォーカス時のメッセージを設定
    ''' </summary>
    ''' <param name=&quot;c&quot;></param>
    ''' <param name=&quot;msg&quot;></param>
    ''' <remarks></remarks>
    Public Sub SETSTATUS(ByVal c As Control, ByVal msg As String)
        _statusdic.Add(c, msg)
        AddHandler c.MouseEnter, AddressOf COM_MouseEnter   'MouseEnter
        AddHandler c.MouseLeave, AddressOf COM_MouseLeave   'MoueeLeave
    End Sub

    Public Sub ChangeFocus(ByVal sender As Object, ByVal b As Boolean)
        If b = False Then
            ToolStripStatusLabel1.Text = &quot;&quot;
        Else
            ToolStripStatusLabel1.Text = _statusdic(CType(sender, Control))
        End If
    End Sub

Control から string を引き出すだけなので Dictionary が使えます。
まあ、List をループさせても、コントロール数はそう多くないので(1万個とかではないでしょう)、スピードは大して変わりません。

というわけで、こんな風に行数を減らしていきます。
今回のポイントとしては、

・冗長な部分を共通化させて、行数を減らす。
・設定なのかロジックなのかを区別する。設定ならば、設定風に書き換える。
・リファクリングをして高速化する。

ってのがミソです。

業務コードの場合、単純にコピー&ペーストで済めば、それを使っても良いのですが、コード自体が冗長になる場合は考え直したほうがよいですね、という話です。このあたりは、保守性も含めると、短いコード(保障されたコードを再利用する)というのが、業務コードを書くときのコツです。

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