ウィリアムのいたずらの、まちあるき、たべあるき

ウィリアムのいたずらが、街歩き、食べ物、音楽等の個人的見解を主に書くブログです(たま~にコンピューター関係も)

依存性の問題の対応策(その1:窓口、引数を一本化してしまう)

2006-11-20 16:55:18 | Weblog

なんか、ツッコミすぎな気もしますけど、前に書いた構成管理とテスト・バグ修正段階における依存性の問題と解決法、で書いた、依存性の問題について、
(引数が修正されても知らない人がいて、コンパイルエラーになることを防ぐ話)

コンパイルを通したときに警告を出して、依存性の問題を知らせる&調べる方法については、

JAVAでAPIが変わったことを示し、コンパイルで落ちないためのDeprecatedとその限界

で書きましたけど、

そもそも、上記依存性の問題のブログを書いたときの対応策としてあげた、「コンパイルエラーを出さないような窓口の一本化」について、書いていないので、そーいう話について、一応書いてみます。

 長い話になるので、今回は1回目です(分割して書きます)




■そもそも、どーいうケースなのか

 簡単な例を挙げます。

●仕様変更前(初めの状態)

aさんは、以下のように、メッセージを出すプログラムをよんでいたとします
public class a {
	public static void main(String[] args) {
		b.errmsg("aのメッセージ");
	}
}


bさんは、実際にメッセージを表示する部分だとします。
public class b {
	public static	void errmsg(String msg,int level)
	{
		System.out.println(msg);
	}
}


dさんも、bさんのメソッドを呼び出していたとします。
public class d {
	public	d()
	{
		b.errmsg("作成");
	}
}


●仕様変更
 これだと、エラーメッセージばっかり出て困るので、エラーレベルというのを付けて、
 それにもとづき出力することにしました。
 エラーレベル 0   デバッグ情報
        1   一般情報
        10  警告
        100 エラー
        999 致命的エラー

 この連絡は、aとbの間でだけ行われました。
 その結果、aは以下のように修正しました。
public class a {

	public static void main(String[] args) {
		b.errLevel	=	10;	//	通常のレベル
		b.errmsg("aのメッセージ",999);	//	999 致命的エラー
	}
}


 bは以下のように修正しました
public class b {
	public static	int	errLevel	=	0;

	public static	void errmsg(String msg,int level)
	{
		if ( level	>=	errLevel)
		{
			System.out.println(msg);
		}		
	}
}

(上記>は、本当は半角です)

●依存性の問題
 でもdには、連絡が行ってないので、まえのまま、

     b.errmsg("作成");

 と呼び出したままです。そうすると、引数が足りないので、コンパイル
エラーになります。

*実際に、こんなに単純な例でミスすることは、まずありませんが、
 話を簡単にするため、これでいきます。

*ちなみに、これのひねりで、
   a=自分たちが作成しているアプリ
   b=PHP
   c=PHP上で動く他社が作ったアプリ
 で、自分たちが、PHPの最新バージョンのものが使いたくて、PHPの
 バージョンを上げたら、他社が作ったアプリが動かなくなった。
 などというケースがあります。




■解決策

こういう問題が起きないための解決策というのは、いくつかあり、
状況によって使い分けます。

1.はじめから、1箇所の窓口に集める
2.古いメソッドを入れておき、落ちるようにする

さらに、問題を軽減する方法として、こんなところで手を打つ場合もあります。
3.引数だけは、共通化する

それと、前回書いた方法
4.古いメソッドをDeprecatedにして入れる
5.古いメソッドをDeprecatedにして入れた上で、落ちるようにする

今回は、まず1について書きます。




■はじめから、1箇所の窓口に集める=仕様概要

それぞれのクラスを呼び出すときは、かならず、

execute(HashMap map)にして、その引数のハッシュマップに設定されている

値に応じて、動きが規定されるとします。

そうすると。。。

●仕様変更前(初めの状態)

aさんは、以下のように、メッセージを出すプログラムを呼ぶことになります。
import java.util.*;
public class a {

	public static void main(String[] args) {
		HashMap	map = new HashMap();
		map.put("job","errmsg");
		map.put("msg","aのメッセージ");
		b.execute(map);
	}
}


bさんは、実際にメッセージを表示する部分だとします。
import java.util.*;

public class b {

	public static	int execute(HashMap map)
	{
		String job;
		
		//	ハッシュマップからジョブ取り出し
		if ( map	==	null )
		{
			throw new RuntimeException();
		}
		job = (String)map.get("job");
		if ( job	==	null )
		{
			throw new RuntimeException();
		}
		
		//	ディスパッチ
		if ( job.equals("errmsg")	==	true)
		{
			String	msg;
			msg = (String)map.get("msg");
			if ( msg	==	null )
				msg	=	"";
			errmsg(msg);
			return	0;
		}

		//	なんにも該当しない
		else
		{
			System.out.println("Jobが設定されていない");
			throw new RuntimeException();
		}
	}
}


dさんも、bさんのメソッドを呼び出していたとします。
import java.util.*;
public class d {

	public	d()
	{
		HashMap	map = new HashMap();
		map.put("job","errmsg");
		map.put("msg","作成");
		b.execute(map);
	}
}


●仕様変更
 変更時に、以下のように、かわります。
aさんのソース
import java.util.*;
public class a {

	public static void main(String[] args) {
		b.errLevel	=	10;
		
		HashMap	map = new HashMap();
		map.put("job","newerrmsg");
		map.put("msg","aのメッセージ");
		map.put("errLevel","999");
		b.execute(map);
	}
}

(赤字が変わったところです)

bさんのソース
import java.util.*;

public class b {
	public static	int	errLevel	=	0;

	public static	int execute(HashMap map)
	{
		String job;
		
		//	ハッシュマップからジョブ取り出し
		if ( map	==	null )
		{
			throw new RuntimeException();
		}
		job = (String)map.get("job");
		if ( job	==	null )
		{
			throw new RuntimeException();
		}
		
		//	ディスパッチ
		if ( job.equals("errmsg")	==	true)
		{
			System.out.println("このやり方は、廃止されました");
			throw new RuntimeException();
		}
		if ( job.equals("newerrmsg")	==	true)
		{
			String	msg;
			int	errLevel = 0;
			msg = (String)map.get("msg");
			if ( msg	==	null )
				msg	=	"";
			try
			{
				String nobuf = (String)map.get("errLevel");
				errLevel = Integer.parseInt(nobuf);
			}
			catch(Exception e)
			{
				e.printStackTrace();
				throw new RuntimeException();
			}
			errmsg(msg,errLevel);
			return	0;				
		}

		//	なんにも該当しない
		else
		{
			System.out.println("Jobが設定されていない");
			throw new RuntimeException();
		}
	}

	private static void errmsg(String msg,int level)
	{
		if ( level	>=	errLevel)
		{
			System.out.println(msg);
		}		
	}
}

(上記< >は、本当は半角です)
赤字の部分は、修正箇所です。

●依存性の問題
 Dさんは、何も修正していませんが、コンパイル上問題ありません。
 なぜなら、Dさんは、変更前executeを呼び出していますが、変更前と後で、このメソッド名と引数は変わってないので、コンパイルは通ります。

 ただし、実行すると、引数のjobに設定されている値が古いerrmsgなので、実行時には、落ちます(なお、落ちるように書いているから落ちるのであり、何もしないように書けば、なにもしない)




■はじめから、1箇所の窓口に集める=メリットデメリット

●メリット
 このように1つのメソッド、変数全体が入る引数でやれば、仕様変更に対しては柔軟にできます。引数の型チェック、機能がコーディングされているかどうかなどは、実質、実行時になるので、コンパイルは通り、構成管理上楽です

●デメリット
 逆に言えば、もし、依存性の問題があった場合、実行時までわからないということになります。
 デグレードさせない回帰テスト(リグレッションテスト)を、確実になっていればいいのですが、そうでないと、以前通った部分は、再度テストされないこともあるので、テストされないから気づかないという危険性が出てきます。

 あと、どんなときでも同じメソッドを呼び出し、引数だけで処理を変えるので、ソースを追いにくいということがあります。
 とんでもないところで、jobを設定してしまうと、読み手は、「一体この処理はなにをやっているんだ?」ということになります。




■はじめから、1箇所の窓口に集める=どこで使うか?

 なので、ここまで過激なものはないのですが、テーブル操作や画面操作で、こういうことをすることがあります。
 引数で、SQLを受け取り、executeでSQLを実行するっていうクラスをつくってしまい、
 どんなテーブルでも、どんなSQLでも、こいつで処理させるというものを作ることがあります。

 画面でexecuteといえば、Struts。Strutsの場合、呼び出し関数がexecuteであるときまっているから、多種多様な画面があっても呼び出せるということになります。
 これを応用して、画面入出力システムを自分で作る場合、全部帰ってきた後の呼び出し先をexecuteにするなんていうきめうちをすることもあります。

 このように、処理レイヤと入出力レイヤなど、2つの大きな処理塊が合って、それが、頻繁に変わったり、予測不能だったり、汎用的に作らないといけない場合、この手法は使われます。
(とくにJavaの場合、リフレクションと組み合わせて使うと、便利便利)


この記事についてブログを書く
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする
« 米ヤフー、幹部がメモで組織... | トップ | YouTubeへの対抗策は、「融合... »
最新の画像もっと見る

Weblog」カテゴリの最新記事