なんか、ツッコミすぎな気もしますけど、前に書いた構成管理とテスト・バグ修正段階における依存性の問題と解決法、で書いた、依存性の問題について、
(引数が修正されても知らない人がいて、コンパイルエラーになることを防ぐ話)
コンパイルを通したときに警告を出して、依存性の問題を知らせる&調べる方法については、
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の場合、リフレクションと組み合わせて使うと、便利便利)