Java Mid Basic〜Lv3_4_リファクタリングLv2 プロパティファイルで無修正ものを作る〜

イントロダクション

前回は、メインメソッドの修正を行わないでのコマンド追加実装をやりました。
具体的には、コンストラクタに作成した、クラスをマップに登録する処理を追加します(1行)。。。
しかし。これもなんだかんだとクラスの修正をしているのでイマイチです。なので

今回は、プロパティファイルを使用します。このファイルは「キー=値」のように記述しているファイルのことで次のような形で作成します。
具体的には、キーをコマンドとして扱い、値を取得するクラスの完全クラス名として記述します。

# キー=値
hello=jp.zenryoku.XXX.HelloCls

目的は、コマンドを入力したら対象のクラスを取得するためです。
<プロパティファイルの扱い>

メインクラスを無修正ものにする

この方向でコマンドを追加する方法を実践したいと思います。その方法は以下の手順で行います。

  1. プロパティファイルにキーとクラスの完全名をセットにして登録する
  2. クラスの完全名からクラスのインスタンスを取得して実行するようにする

上記のような形になります。「よくわからん。。。」と思った方、詳細を下に記載します。

修正のいらない実装

修正をしなくても「クラスを追加」もしくは、「新しい機能の実装を追加」するためには「依存関係を分断」してやる必要があります。

世間巷では「依存関係の分断」なんて言葉が一時流行りました。これは下のようなプログラムコードがあったとしましょう。

無修正Lv1

MyCls main = new MyCls();
String param = "hello";
main.execute(param);

上のようなコードの場合は、「MyCls」をコード上で「new」しているので、「MyCls」を変更して別のクラスを使用したい時にはこのコードを書き換える必要があります。

無修正Lv2

しかし、下のようにプログラムを書き換えてやれば、「getMyCls」の中身を修正すればよく、下のコードは修正しなくてよくなります。

MyIF main = getMyCls();
String param = "hello";
main.execute(param);

無修正Lv3

上のコードを応用していくと、次のように書くことも可能です。これはjava.lang.refrectionというパッケージを使ったやり方です。

Class<? extends MyIF> cls = Class.forName("完全クラス名");
MyIF main = (MyIF) cls.getInstance();
String param = "hello";
main.execute(param);

上のように書けば、「完全クラス名」を動的に渡してやれば目的のクラスのインスタンス(オブジェクト)が取得できます。具体的には完全クラス名をメソッドの引数に渡してやる方法です。次のようなコードも使えます。

public MyIF getMyIF(String perfectClsName) {
    Class<? extends MyIF> cls = Class.forName("完全クラス名");
    MyIF main = (MyIF) cls.getInstance();
    return main;
}

public void sample() {
    MyIF main = getMyIF("jp.zenryoku.XXX.HelloCls");
    String param = "hello";
    main.execute(param);
}

ちょっと、わかりずらいかもしれませんが、このように書くとプログラムを修正しなくても目的を果たすことができます。
※目的は、対象のインスタンスを取得するということです。

はじめは「MyCls main = new MyCls()」と書いていたものを書き換えて無修正ものにしました。
このようにプログラムコードを修正しなくても使いたいオブジェクト(インスタンス)を動的に取得できるようにすることを「依存関係の分断」といいます。

ちなみに、Springframeworkというフレームワークではこの「依存関係の分断」をDI(Dependency Injection)という形で実現しています。

依存関係の分断

依存関係とは「関連性がある」ということ、つまり「関連性がある=片方を修正したら、もう片方も修正する必要がある」という関係が出来上がっていることです。
よく世間では「依存関係の注入」などという言葉で言いますが、実際どういうことか?に関して触れていることが少ないように感じています。
実装方法に関してはいろんな記事があり、わかりやすく説明がされていると思います。

つまり

依存関係を分断しようということです。具体的には下のように考えます。

  1. 新しい機能を追加するときには今実装している部分を修正したくない
  2. 新しく機能を追加するのと、実装中のクラスを修正しない方法は?

大雑把に、上のように考えます。サンプルとして今回のサンプルアプリを参考にして考えてみます。
現状の実装はGitにあるソースを見てもらうと一目瞭然です、一部抜粋して以下に示します。(メインメソッドのみ抜粋)

public static void main(String[] args) {
    // コマンドの用意
    cmdMap = new HashMap<String, CommandIF>();
    cmdMap.put("hello", new HelloCommand());
    cmdMap.put("ready", new ReadyCommand());
    // 標準入力
    Scanner input = new Scanner(System.in);

    while(true) {
        System.out.println("入力してください: ");
        String inStr = input.nextLine();
        CommandIF cmd = cmdMap.get(inStr);
        if (cmd != null) {
            cmd.execute();
        }
        if ("bye".equals(inStr)) {
            System.out.println("Good Byw");
            break;
        }
    }
}

動きは下の動画のようになります。

この状態では、Mapにコマンドクラスを追加してやれば新しい機能を追加できます。なので修正するポイントはコンストラクタのみになります。

この際だから。。。

そう、「ここまできたら最後までいけ!」と思うのが人情(自分だけでしょうか?(笑))、なのでやってしまいましょう。

<実装手順>

  1. コンストラクタでMapにコマンドクラスを追加するのをやめる
  2. その代わりにプロパティファイルを使用する
  3. プロパティファイルから完全クラス名を取得し、インスタンスを生成
  4. 生成したインスタンスのメソッドを実行する

このようにしてやれば、メインクラスの修正はいりません。
<追加するもの>
・プロパティファイル
・プロパティファイルのロード処理
・同様に値の取得処理

<CommandList.properties>

 hello=jp.zenryoku.sample.lv3.refactor.cmd.HelloCommand
ready=jp.zenryoku.sample.lv3.refactor.cmd.ReadyCommand
</pre>
<pre>/** プロパティファイル取得 */
public void loadPropertyFile() {
    prop = new Properties();
    try {
        // resources/
        prop.load(getClass().getResourceAsStream("/CommandList.properties"));
    } catch (IOException e) {
        e.printStackTrace();
        // エラーコード-1をセットしてプログラム終了
        System.exit(-1);
    }
}

プロパティファイルからインスタンスの取得

public CommandIF getCommandIF(String key) {
    // 完全クラス名を取得する
    String fullClassName = prop.getProperty(key);
    if (fullClassName == null || "".equals(fullClassName)) {
        return null;
    }
    CommandIF cmd = null;
    try {
        @SuppressWarnings("unchecked")
        Class<CommandIF> cmdCls = (Class<CommandIF>) Class.forName(fullClassName);
        cmd = cmdCls.newInstance();
    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        System.exit(-1);
    }
    return cmd;
}

このように、メインメソッドの修正を行わなくてもCommandIFを実装したクラスの数だけコマンドに対する実行処理を作成することができます。

その代わり、プロパティファイルにキーと値を追加する必要がありますが。。。しかしコードを修正するとテストを行う必要があるので格段に余計な手間と時間を省くことができます。

まさに「無修正」

できた時間を使用して、心と体のリフレッシュ及び、楽しい時間を過ごす。。。これが平和への第一歩だと思います。
このように、時間を大切に使わないと、昔の不況時代のように「寝る暇がない!」とか「今日も漫画喫茶でお泊まり!」とか。。。おおよそ人間の尊厳が無いような、不幸なことになってしまうので、日々精進を重ねることが大切だと思います。。。趣味にしてしまうのが一番手っ取り早いという噂もあるとか無いとか(笑)

でわでわ。。。

投稿者:

takunoji

音響、イベント会場設営業界からIT業界へ転身。現在はJava屋としてサラリーマンをやっている。自称ガテン系プログラマー(笑) Javaプログラミングを布教したい、ラスパイとJavaの相性が良いことに気が付く。 Spring framework, Struts, Seaser, Hibernate, Playframework, JavaEE6, JavaEE7などの現場経験あり。 SQL, VBA, PL/SQL, コマンドプロント, Shellなどもやります。

コメントを残す