• 2019年5月3日金曜日
アリスト戦記
アリスト戦記 https://blog.aristo-solutions.net/2019/05/javatemplatebatchspringboot_3.html

SpringBootのDI(依存性注入)をシステム日付で説明してやるぜ

SpringBoot……と言うかSpringFramework伝統の必殺技「DI(依存性注入)」だが、これが難しい!!
これが分かってるプログラマーなんて滅多におらんぞ!!

僕も手探りで何とかボチボチ使い方程度は……、くらいの理解度だけど、DIとは一体何なのか?
それを「システム日付」を題材に説明しよう。

システム日付とは?

システム日付とは何か?


(´・ω・`)「Javaでnew Date()して取れる値のことですよ」


違うだろ!!


そのシステムにおいて「システム日付」とされるものには定義があるんだ。


  • ある時は、シンプルに「new Date()」で取れた値とする。
  • ある時は、DBに向かってSQLを発行して取得された時刻とする。
  • ある時は、タイムサーバからネットワーク経由で取得した時刻とする。


と言うように、「システム日付」と一言に言ってもそれが何なのかは微妙なものなんだ。
実行環境のサーバ時刻とした場合でも、1号機と2号機で時間が違っているかもしれないしな。


(´・ω・`)「いや、NTPサーバで同期しているから1号機と2号機の時間は同じなんで問題無いんですよ」


それが違うんだ!!

その考え方を「結合」って言うんだ。
それは結合環境とか本番環境での結合テスト、システムテストの段階で問題が起きていないかを確認する為の着眼点であり、製造環境でそんな考えをしていてはいけない。

あくまで「単体」として完成させるのが製造の基本である。

そこの考え方がゴッチャになっていると、じゃあ、平成最後の2019年4月30日と、令和元年の5月1日の境界値のテストを製造環境でやる場合はどうすんの?


(´・ω・`)「パソコンの時間を変えれば良いんですよ」


だからそれを「結合」って言うんだ!!


「外部環境がこういう風になっていることが前提」という状況でなければテスト出来ないのは単体テストではなく結合テストである。


こういう風に考察していくと、システム日付の正体は外部資源であることが分かるだろう。

外部資源と聞くと「データベース」や「ファイル」を思い浮かべるだろうが、実際にはJava仮想マシンの外側にあるサーバの日付の値を貰ってくる処理であるシステム日付取得は立派な外部資源ってわけよ。
「Java標準のクラスであるnew Date()で簡単に取れるから外部資源ではない」みたいに安易に騙されてしまわないように注意しなければならない。

従って、外部資源なら外部資源として、システム日付取得もまた疎結合を意識したクラス設計が為されているべきと言える。

MVCモデルのプロジェクトで「M」に相当するデータベースがコントローラから分離されているのと同じように、システム日付もまた、コントローラからの分離を想定したクラス設計になっていなければならない。

ここまではお分かり頂けただろうか?

システム日付マネージャー作成

次に、その「疎結合なシステム日付取得処理」とはどういうものかをご説明しよう。

以下3つのクラスを作成する。


  • 普通のシステム日付が取れる。
  • 平成最終日である2019年4月30日で固定で取れる。
  • 令和最初の日である2019年5月1日で固定で取れる。


元号切り替えのリハーサルをやるイメージなわけだ。
ソースはそれぞれこうなる。

普通のシステム日付
@Component("net.aristo.template.batch.common.util.SystemDateManagerSystemDate")
public class SystemDateManagerSystemDate implements SystemDateManager {

   /** logger */
   private static final Logger logger = LoggerFactory.getLogger(SystemDateManagerSystemDate.class);

   @Override
   public Date getDate() {

      logger.debug("システム日付を取得します。");

      return new Date();

   }
}


平成最終日固定
@Component("net.aristo.template.batch.common.util.SystemDateManagerHeiseiLast")
public class SystemDateManagerHeiseiLast implements SystemDateManager {

   /** logger */
   private static final Logger logger = LoggerFactory.getLogger(SystemDateManagerSystemDate.class);

   @Override
   public Date getDate() {

      logger.info("平成最終日モードで日付を取得します。");

      Calendar c = Calendar.getInstance(TimeZone.getTimeZone("JST"));
      c.clear();
      c.set(2019, 4 - 1, 30, 23, 59, 59);

      return c.getTime();

   }
}


令和最初固定固定
@Component("net.aristo.template.batch.common.util.SystemDateManagerReiwaFirst")
public class SystemDateManagerReiwaFirst implements SystemDateManager {

   /** logger */
   private static final Logger logger = LoggerFactory.getLogger(SystemDateManagerSystemDate.class);

   @Override
   public Date getDate() {

      logger.info("令和元年最初モードで日付を取得します。");

      Calendar c = Calendar.getInstance(TimeZone.getTimeZone("JST"));
      c.clear();
      c.set(2019, 5 - 1, 1, 0, 0, 0);

      return c.getTime();

   }
}


注目点は、それぞれ「@component」のアノテーションと「SystemDateManager」のインターフェースを継承しているということ。

インターフェースの方はこちら。

@Component
public interface SystemDateManager {

   /**
    * システム日付を取得する。
    *
    * @return システム日付
    */
   public Date getDate();

}


ここからが問題だ。

やりたいこととしては、


  • 通常は普通にシステム日付を取得したい。
  • しかし、テスト用に日付が「平成最終日」「令和最初の日」で固定されたモードに切り替えたい。


この「切り替え」をどうやってやるのか?
それが「DI(依存性注入)」だ。

呼び出し側ではインターフェースを宣言しておき、その中身に何を注入するのかをSpringBootの機能で切り替える、というわけだ。

呼び出し側

呼び出し側はこんな感じになる。

@SpringBootApplication
public class SystemDateExecuter extends AbstractExecuter {

   /** logger */
   private static final Logger logger = LoggerFactory.getLogger(SystemDateExecuter.class);

   @Resource(name = "${SystemDateManager}")
   @Autowired
   private SystemDateManager systemDateManager;

   public static void main(String[] args) throws Exception {

      ConfigurableApplicationContext context = SpringApplication.run(App.class, args);

      SystemDateExecuter exe = context.getBean(SystemDateExecuter.class);
      exe.start(args);

   }

   @Override
   public void doStart(String[] args) {

      logger.info("システム日付={}", systemDateManager.getDate().toString());

   }

}


ポイントはココだ。
@Resource(name = "${SystemDateManager}")
@Autowired
private SystemDateManager systemDateManager;


注入するクラスが一個しか無い場合は「@Autowired」だけで良いんだけど、同じインターフェースの配下に複数のクラスがある場合は、「@Resource」で注入クラスを指定する必要がある。

「${SystemDateManager}」の部分のそのクラスを書くことになる。

クラスを指し示す一意キーに相当するものは、@compornetに書かれている値だ。


  • @Component("net.aristo.template.batch.common.util.SystemDateManagerSystemDate")
  • @Component("net.aristo.template.batch.common.util.SystemDateManagerHeiseiLast")
  • @Component("net.aristo.template.batch.common.util.SystemDateManagerReiwaFirst")


値は一意でありさえすれば何でも良いのだが、やっぱりクラスのフルパスをキーにしてしまうのが一番手堅いのではないだろうか?

つまり、

@Resource(name = "net.aristo.template.batch.common.util.SystemDateManagerSystemDate")

と書けば注入クラスを指定出来る、というカラクリだ。

プロパティファイルで切り替え

しかし、「@Resource(name = "net.aristo.template.batch.common.util.SystemDateManagerSystemDate")」と書いてしまうと、ソースにそのクラスが固定で書かれているということになる。

出来ればプロパティファイルで切り替えたい。
プロパティファイルで切り替える為には、注入対象の部分を変数化する必要がある。
それが「${SystemDateManager}」の意味だ。

そして、application.propertiesに以下のように書けばOK。

#システム日付モード切替
SystemDateManager=net.aristo.template.batch.common.util.SystemDateManagerSystemDate
#SystemDateManager=net.aristo.template.batch.common.util.SystemDateManagerHeiseiLast
#SystemDateManager=net.aristo.template.batch.common.util.SystemDateManagerReiwaFirst


これで、プロパティファイルの切り替えでシステム日付モードを変更出来る。

活用法

もう分かるよね?

「平成最終日」と「令和元年」でシステムが正しく動いているか確認したいという場合、プロパティファイルを切り替えればシステム全域が一斉にそのモードに切り替わるという寸法よ。

もちろん、最終的には結合環境でシステム日付を動かしてテストするべき話ではあるが、単体環境では上記のように注入クラスを差し替えることでJUnitを動かすことが出来る。

「元旦」「大晦日」「うるう年」「うるう秒」など、実行日時の影響を受けるテストをやりたい場合は全部同じ要領でやれるってわけだ。

if文方式より綺麗

実際のところ、こういうモード切替はDI注入を使わずとも、if文でのフラグ切り替えでも表現出来る。

しかし、if文方式よりも、DI方式の方が綺麗なのよね。

上記のサンプルは「通常モード」「平成モード」「令和モード」の3つしか無いけど、もっと数が増えた場合、

if...else if...else if...else if...else if...else if

と、数が増えるごとにif分追加ではソースが汚くなる。

DI方式だと、それぞれ別のクラスにした上で、プロパティファイルの切り替えで対応可能だから、保守性が非常に高い。

それがDIのメリットだ。

終わりに

以上になるが、これでDIの価値をお分かり頂けただろうか?

保守性の高いソースを作るためにDIは非常に強力な武器になる。
ちょっと難しい話ではあるが、ぜひ理解してプロジェクトに生かして貰いたい。

0 件のコメント:

コメントを投稿