Javaプログラミング能力認定試験課題プログラムのリファクタリングレポート(その3)
少し間が開いてしまいましたが、前回のJavaプログラミング能力認定試験課題プログラムのリファクタリングレポート(その2)に続いて、試験問題のリファクタリングについて説明します。
Template Methodデザインパターンを使った制御の反転
前回までのリファクタリングで、
- 全体的にレイヤー化されたパッケージ構造を規定
- わかりにくい変数名やクラス名をリネーム
- Repository、Consoleなど複雑で再利用可能な処理をインタフーフェースとその実装クラスとして抽出
- 各機能を担当するアプリケーション層のクラスに共通のインターフェースを実装させることでポリモーフィック(多態的)に処理を起動
といった手順を実行してきました。これまでのリファクタリングで、オリジナルのソースコードに比べると飛躍的に可読性の高いコードになっていると思いますが、ところどころで同じような制御構造が繰り返し出現することが気になります。つまり、達人プログラマーのDRYの原理に違反しているということです。
たとえば、メインクラスでは以下のような制御構造が現れます。
public void run() { while (true) { beforeDisplayMenu(); String inputCode = printMenuAndWaitForInput(); if (isEndCommand(inputCode)) { // 終了 break; } runFunction(inputCode); } } @Override protected void runFunction(String inputCode) { Function subFunction = functionMap.get(inputCode); if (subFunction == null) return; assert subFunction != null; try { subFunction.run(); } catch (Exception ex) { // TODO 適切な例外処理 ex.printStackTrace(); } if (isConfirm(inputCode)) { console.accept("エンターキーを押すとメニューに戻ります。"); } }
つまり、「Eキー」が入力されるまで無限ループして、機能を呼び出すという処理を繰り返しています。このような処理はメインクラスだけでなく、各サブ機能を呼び出すクラスの中にも何回か登場しますし、また、別のコンソールアプリケーションでも必ず出現するものです。従来の言語では継承が利用できないため、このような場合には通常の手段ではコピペや生成ソースコードの自動生成に頼らざるを得ません。しかし、Java言語の場合、継承と抽象メソッドを利用することで汎用の制御構造を親クラスに定義することができます。そして、複数のサブクラス間で簡単にロジックを再利用することができます。実際、今の場合、次のような抽象クラスを定義することができます。
public abstract class AbstractMainProgram implements Runnable { protected Map<String, Function> functionMap = new HashMap<String, Function>(); protected Console console; public void run() { while (true) { beforeDisplayMenu(); String inputCode = printMenuAndWaitForInput(); if (isEndCommand(inputCode)) { // 終了 break; } runFunction(inputCode); } } protected void beforeDisplayMenu() {} protected boolean isEndCommand(String inputCode) { return "E".equals(inputCode); } protected abstract String printMenuAndWaitForInput(); public Map<String, Function> getFunctionMap() { return functionMap; } public void setFunctionMap(Map<String, Function> functionMap) { this.functionMap = functionMap; } protected boolean isConfirm(String inputCode) { return !"S".equals(inputCode); } /** * 機能一覧と機能コード一覧を表示し,機能コードを取得して該当の機能を呼び出す */ @Override protected void runFunction(String inputCode) { Function subFunction = functionMap.get(inputCode); if (subFunction == null) return; assert subFunction != null; try { subFunction.run(); } catch (Exception ex) { // TODO 適切な例外処理 ex.printStackTrace(); } if (isConfirm(inputCode)) { // 人材管理と稼働状況管理のみ console.accept("エンターキーを押すとメニューに戻ります。"); } } }
このクラスのrun()メソッドでは固定の制御構造が記述されていますが、このメソッドから複数の抽象メソッドやフックメソッドが起動されています。この場合のrun()メソッドのように複数の抽象メソッドの呼び出し順序を規定するメソッドはTemplate Methodとして知られていますが、特に有名なGoFのデザインパターンの中でももっとも有名なものの一つとして知られています。
このような親クラスが定義できると、後はサブクラスで必要な抽象メソッドやフックメソッドをオーバーライドするだけで済むようになるのです。以下の例を見てください。
public class TempHRManagementProgram extends AbstractMainProgram { /** * 機能一覧 */ private static final String[] MENU_LIST = { "_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/", " 人材管理システム", " メニュー", " [1].人材検索(S)", " [2].人材管理(JI:追加 JU:更新 JD:削除)", " [3].稼働状況管理(KI:追加 KD:削除)", " [4].終了(E)", "_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/"}; /** * 機能コード一覧 */ private static final List<String> CODE_LIST = Arrays.asList( "S", "JI", "JU", "JD", "KI", "KD", "E" ); @Override protected String printMenuAndWaitForInput() { console.display(""); //改行 console.display(MENU_LIST); return console.acceptFromList(CODE_LIST, "どの機能を実行しますか?"); } ... }
このサブクラスでは、このアプリケーションに固有のメニュー文字列を定義して、表示するprintMenuAndWaitForInput()メソッドのオーバーライドが行われていますが、その他の基本的な制御構造は親クラスから継承したTemplate Method内で行われているため、サブクラスで定義する必要がありません。
確かに、このようなテクニックに慣れていないと、全体の制御の構造が追いかけにくいと感じる人もいるかもしれませんが、制御構造を決めるのは親クラス(一般にフレームワーク)であり、アプリケーションはフレームワークから呼び出されたタイミングで必要な処理を行うコードを記述するという流れになります。ちょっと受動的な感じですが、上から指示されて初めて何かをする人のように、全体の制御の流れは汎用的なコード内で行い、具体的なロジックは呼び出されたタイミングで実行されるのです。この考え方は制御の反転(Inversion of Control、IoC)と呼ばれており、オブジェクト指向プログラミングを行う上で欠かすことのできないテクニックとなっています。
なぜ、IoCが重要なのでしょうか?いろいろなポイントがあると思いますが、理解すべき重要なこととして、IoCにより無駄な重複ロジックが共通化されるだけでなく、関心事が分離されるということが大切だと思います。つまり、この例だとメニューコマンドの入力を受け取って、それによりサブ機能を呼び出すという部分は、フレームワークを作成するプログラマーが理解していればよく、一般に個別のアプリケーションロジックを記述するプログラマーが意識する必要のない部分となるのです。逆にアプリケーションロジックを記述するプログラマーはメニュー構成の文字列など具体的な仕様に意識を集中できるようになります。この関心事の分離によって、フレームワーク作成者、アプリケーションロジック作成者の双方にとってプログラムは理解しやすいものとなり、また、おのおの独立して機能を拡張しやすい構造が提供されるのです。
DIコンテナーの導入
古典的なJavaのプログラムでは以上のようなデザインパターンを利用することで制御の反転を実現することが普通でした。ただし、現在ではSpring FrameworkのようなDIコンテナーと呼ばれるフレームワークを利用することで、IoC的な設計をさらに簡単に実現することができるようになっています。本来、試験問題のリファクタリングとしてはこのようなフレームワークを利用することはズルなのかもしれませんが、実際のアプリケーション開発でDIやそれに類する技術を使わないということはほとんどなくなってきていますし、教育目的からもDIを使った方が好ましいと思いますので、あえてSpring Frameworkを導入することにしました。以下は人材情報の削除機能を実現するクラスです。
/** * 人材情報削除 */ @Component public class DeleteHRFunction implements Function { @Inject private HumanResourceRepository hrRepository; @Inject private Console console; @Inject private HumanResourceView hrView; private HumanResource selectedHumanResource; /** * 人材管理(削除)メニューの実行 */ public void run() { selectHumanResource(); deleteHumanResource(); } private void selectHumanResource() { // 人材ID入力 long hrId = console.acceptLong("人材IDを入力してください。", new ValidInput<Long>() { @Override public boolean isValid(Long input) { // 人材ID存在チェック return hrRepository.findById(input) != null; } }); selectedHumanResource = hrRepository.findById(hrId); hrView.display(selectedHumanResource); } private void deleteHumanResource() { if (console.confirm("この人材情報を削除しますか?(Y はい N いいえ)", "Y", "N")) { hrRepository.delete(selectedHumanResource.getId()); console.display("削除しました。"); } } }
クラスのフィールド宣言を見るとわかるように、このクラスの実行にはファイル入出力を行うHumanResourceRepositoryと画面出力を行うConsoleの実装クラスと相互にやり取りすることが必要になります。前回までのリファクタリングでやったように、普通オブジェクト指向プログラミングでは、それぞれの役割ごとに適切なクラスを抽出し、それぞれに処理を担当させることでプログラムが動作するため、あるクラスを実行させるには、そのクラスが依存している他のクラスのインスタンスを生成し、準備してやらなくてはなりません。しかし、そうした準備は本来このクラス自身で行いたい本質的なロジックそのものとは無関係のものです。SpringのようなDIコンテナーを使うと、このように依存関係にあるオブジェクトを自動的に生成し、フィールドに代入(=インジェクション)してくれます。実際、上記の例のように@Injectというアノテーションをつけておくだけで、すべて自動的に代入が行われるのです。
前節のTemplate Methodパターンの例と同じで、全体の処理の流れは見えなくなっていますが、このクラスで実現したい本質部分はより明確になっていると思います。
なお、DIについては、依存関係の自動化によるコードの簡易化ということ以外に、インターフェースに対するコーディングを容易化し、クラス間の結合度を下げることで単体試験を容易にするといったメリットなど他にもさまざまな効果がありますが、ここでは説明は割愛させていただきます。(Web上で探せば、今ではたくさんの日本語の入門記事が見つかると思います。)
ここまでのリファクタリングの結果
参考までに、ここまでのリファクタリングの結果を以下に添付させていただきます。(ビルドするためにはMaven2の実行環境が必要です。)
refactored.zip
(最新版はGitHubで公開しています。GitHub - ryoasai/certification-refactoring: Java認定試験のリファクタリングサンプル)
もちろん、あくまでもこれはリファクタリングの例に過ぎないので、別の形になることも大いにあり得ると思いますが、基本的なOOPのテクニックは参考にしていただけると思います。
さらなる課題
これまでのリファクタリングで相当コードは単純になりましたが、コードが単純化されるとやりたいことが新しく見つかるということもあります。
- 単体試験を作成する。(本来はテストファーストでやるべきですが)
- 配列とリストなど整合性のとれていないAPIを統一する
- 単一責任原則(SRP)にしたがっていないメソッドやクラスの分割を調整する
- 「display」と「print」などのボキャブラリーのゆれを統一する
- コンソールクラスを拡張して、画面ログを保存したり、入力補完したりする機能を付加する
- Webインターフェースを作ってみる
- データをテキストファイルからDBに移行する
- 複数ユーザーの同時操作を可能にする(マルチスレッド対応)
- データアクセス層でメモリ上のキャッシュによる高速化を実現する
- アプリ層のクラスをGroovyなど軽量言語で記述することによりさらに簡易化する
- 画面構成の作成部分をDSL的にもっと簡単に定義できるようにする。
- アノテーションなどのメタ情報を用いてPojoのフィールドと文字列配列との変換を自動化する
結局、保守に忍耐力が必要だったオリジナルのコード(SI業界(日本)のJavaプログラマーにはオブジェクト指向より忍耐力が求められている?)とのもっとも重要な違いは、プログラマーが楽しんで機能を追加できるようになるというところにあるのではないでしょうか?業務システムの開発でも、こうした楽しいプログラミングができるような環境になれば、プログラマーが幸せになれるのはもちろんのこと、高い品質と拡張性を手にいれられるユーザーにとってもメリットがあるのであり、Win-Winでお互いに幸せになれるはずなのです。そのためには、ちょっとした社員教育とパラダイムシフトの導入を行えばよいのであり、どの会社でも決して不可能なことではないと信じます。