イントロダクション
前回アプリケーションを作り、それを運用すること、拡張することについて考えてみました。
まとめると次のようなことが必要になるということを記載しました。
- 各プログラム間(Javaのクラス同士)の依存度を限りなく低くする。
- プログラムを拡張するのに、元のプログラムをほとんど変更しない。
- Javaを使えるレベルの知識(技術)がある人なら、誰が見てもわかるようなコードを書く。
- 各クラス(部品)の単体テスト(UnitTest)ケースを用意しておき、修正したら即テスト、部品を取り換えるだけでよいように、作成した資源(プログラムのコード)を結合テスト・総合テストと実施できるような体制を整える
今回は、プログラムを拡張していくための基本になる考え方と実践方法について記載していきたいと思っています。
例として「〇×あてゲーム」を拡張していく形でプログラム(クラス)間の依存度を低くした形のプログラミング方法について記載していきたいと思います。
コードを修正しなくても良い形を作る
public class Lv2Main {
/** 終了フラグ */
private static boolean isFinish;
public static void main(String[] arg) {
isFinish = false;
// 標準入力を受け取るクラスをインスタンス化
Scanner scan = new Scanner(System.in);
while (true) {
System.out.println("「〇×あてゲーム」 0: 〇 1: ×。");
// 標準入力を受け取る
int input = scan.nextInt();
// isFinishがtrueならば処理終了。
if (isFinish) {
System.out.println("プログラムを終了します。");
break;
}
// 0か1の値を返却する
Random rdm = new Random();
int res = rdm.nextInt(1);
// 0: 〇 1: ×で当たったかどうかの判定
boolean isAtari = res == input;
String value = input == 0 ? "〇" : "×";
if (isAtari) {
System.out.println("あたり:" + value);
} else {
System.out.println("はずれ:" + value);
}
System.out.println("続けますか? 0: 続ける 1: やめる");
int next = scan.nextInt();
if (next == 1) {
break;
}
}
}
}
この状態のプログラムはよく見かける。。。と思われる形のプログラムです。
つまりは、処理を1つのファイル内にすべて書いている形のプログラムということです。
このブログ的に表現すると「Javaの基本:上巻」に当たるプログラムの書き方になります。
「Javaの基本:下巻」に当たるプログラムの書き方はこれから説明していきます。
Step1. 役割分担を行う
上のプログラムで行っていることを、例えばチームで作業をするように、役割分担を行いそれぞれのクラスにそれぞれの役割を与えます。
例えば、次のような役割分担を行います。
- 〇×あてゲームを起動する役割
- 標準入力を受け取る、などの標準入出力をコントロールする役割
- 〇×あてゲームの各種判定を行う役割
もともとのプログラムは、全部の処理が書いてあるので、これを切り貼りして改造します。
Step2. クラスを作成する
上記の通り、2つの役割を担当するクラスを作成します。
役割分担を確認
クラス名 | 役割 |
---|---|
Lv2Main ※作成済み | 〇×あてゲームを起動する役割 |
MarubatsuConsole | 標準入力を受け取る、などの標準入出力をコントロールする役割 |
MarubatsuUtils | 〇×あてゲームの各種判定、ユーティリティの提供を行う役割 |
これらのクラスに必要な処理を切り貼りして、各クラスに移植します。具体的には、作成したコードを切り貼りして、それぞれの役割のクラス内のメソッドに移植します。
その結果を以下に記載致します。
※リンクはGithubにアップしたコードです。
<Lv2Main>: 〇×あてゲームを起動
package jp.zenryoku.tutorial.level2;
import java.util.Scanner;
/**
* Javaの基本レベル2:小さなレベルのプログラムを拡張する。
* 今回は、〇か×か当てるゲーム。
*/
public class Lv2Main {
/** 終了フラグ */
private static boolean isFinish;
public static void main(String[] arg) {
isFinish = false;
// 標準入力を受け取るクラスをインスタンス化
Scanner scan = MarubatsuUtils.getScanner();
while (true) {
// ゲーム開始文言
MarubatsuConsole.printGameStart();
// 標準入力を受け取る
int input = scan.nextInt();
// isFinishがtrueならば処理終了。
if (isFinish) {
MarubatsuConsole.printTerminated();
break;
}
// 0か1の値を返却する
int res = MarubatsuUtils.nextInt(2);
// 0: 〇 1: ×で当たったかどうかの判定
boolean isAtari = MarubatsuUtils.judgeAtariOrNot(res, input);
MarubatsuConsole.printAtatiorNot(isAtari, input);
// 続けるのかどうか判定する
if (MarubatsuConsole.printNextPlayOrNot(scan)) {
break;
}
}
}
}
<MarubatsuConsole>:標準入出力をコントロール
※修正した後のコードがアップしてあります。
package jp.zenryoku.tutorial.level2;
import java.util.Scanner;
public class MarubatsuConsole {
/**
* 〇×あてゲームの開始文言を表示
*/
public static void printGameStart() {
System.out.println("「〇×あてゲーム」 0: 〇 1: ×。");
}
/**
* 〇×あてゲームの終了文言を表示
*/
public static void printTerminated() {
System.out.println("プログラムを終了します。");
}
/**
* 当たったかどうかを表示する。
* @param isAtari 当たり判定の結果
*/
public static void printAtatiorNot(boolean isAtari, int input) {
String value = input == 0 ? "〇" : "×";
if (isAtari) {
System.out.println("あたり:" + value);
} else {
System.out.println("はずれ:" + value);
}
}
/**
* 〇×あてゲームを続けるかを表示、Yes or Noを取得する
* @param scan
* @return true: 続ける false: やめる
*/
public static boolean printNextPlayOrNot(Scanner scan) {
boolean playNext = false;
System.out.println("続けますか? 0: 続ける 1: やめる");
int next = scan.nextInt();
if (next == 1) {
playNext = true;
}
return playNext;
}
}
<MarubatsuUtils>: ユーティリティの提供を行う役割
つまるところは、「便利機能を提供するクラス」ということです。
※修正した後のコードがアップしてあります。
package jp.zenryoku.tutorial.level2;
import java.util.Random;
import java.util.Scanner;
public class MarubatsuUtils {
/** 標準入力を受け取るクラス */
private static Scanner scan;
/** 乱数の生成クラス */
private static Random rdm;
/**
* 標準入力を受け取るクラスを生成、取得する。
* ※シングルトン実装
* @return Scanner
*/
public static Scanner getScanner() {
if (scan == null) {
scan = new Scanner(System.in);
}
return scan;
}
/**
* 乱数生成クラスがインスタンス化されていなければ、インスタンス化します。
* すでにインスタンス化しているときは、既存のインスタンスを使用します。
* ※シングルトン実装
*
* @param bound
* @return 生成した乱数
* @see <a href="https://docs.oracle.com/javase/jp/8/docs/api/java/util/Random.html">Random</a>
*/
public static int nextInt(int bound) {
if (rdm == null) {
rdm = new Random();
}
return rdm.nextInt(bound);
}
/**
* 生成した乱数と、入力値が等しいか判定する。
*
* @param res 生成した乱数
* @param input 入力値
* @return true: 等しい false: 違う
*/
public static boolean judgeAtariOrNot(int res, int input) {
// 0: 〇 1: ×で当たったかどうかの判定
return res == input;
}
}
- メインメソッドは、すっきりしたコードになったと思います。
- メインメソッドでは、どんな処理をしているかが一目瞭然になり。各クラスはその処理だけが書いてある形になります。
もちろん、引数と返り血があるので、呼び出し元に多少なりとも影響が出ます。
しかし、これでメインメソッドを変更しなくても〇×あてゲームを拡張する準備ができました。
ポイント1
これでプログラムの修正するときには、それぞれのクラス、メソッドを修正してやればよくなりました。
メインメソッドは修正する必要がなくなったということです。
ポイント2
メインメソッドには、大まかな処理の順序を書く。具体的には下のような形です。
// <無限ループ開始>
// ■ゲーム開始文言を表示
// ■標準入力を受け取る
// ■isFinishがtrueならば処理終了。
// ■0か1の値を返却する
// ■0: 〇 1: ×で当たったかどうかの判定
// ■続けるのかどうか判定する
<やったこと>
- これらの処理をはじめの状態では、すべてLv2Mainクラスに記述していましたが、これをクラス別に分けました。
- 作成した各クラスをメインメソッドで呼び出して実行する。この時に初めの動きと変わらないことを確認しました。
これにより、プログラムのコードがすっきりして(したと自分は思います。。。)、メインメソッドを修正しなくても次の部分の処理が、修正が可能になりました。
1. ■ゲーム開始文言を表示: MarubatsuConsoleの修正
2. ■標準入力を受け取る: MarubatsuUtilsの修正
3. ■0か1の値を返却する: MarubatsuUtilsの修正
4. ■0: 〇 1: ×で当たったかどうかの判定: MarubatsuUtilsの修正
5. ■続けるのかどうか判定する: MarubatsuConsoleの修正
「そのように作ったんだから当たり前だろ?」と思った方、正常でございます。
このような作り方が、クラスを拡張する、アプリケーションを拡張するときに、役立つのです。
今回の「〇×あてゲーム」のような小さなレベルのプログラムはファイル一枚で何も問題はありませんが、会社の業務で使用するような大きなアプリケーションでは、いろんな機能が必要なので、役割分担を行い、機能拡張がしやすい形で、プログラムを組んでいきます。
拡張する準備をする
初めに「必要になること」について記載したのですが、触れていない部分があります。次の部分です。
- 各クラス(部品)の単体テスト(UnitTest)ケースを用意しておき、修正したら即テスト、部品を取り換えるだけでよいように、作成した資源(プログラムのコード)を結合テスト・総合テストと実施できるような体制を整える
具体的にどのようなことか?これについて、記載したいと思います。
JavaならばJUnit
JavaのテスティングフレームワークといえばJUnitです。
具体的に、どのように使うか記載したいと思います。
役割分担をしたクラスのテストクラス作成
作成した各クラスのテストケースを作成するためのクラスを作成します。
クラス名は、「対象のクラス名 + Test」の形で作成します。
- MarubatsuConsole: MarubatsuConsoleTest
- MarubatsuUtils : MarubatsuUtilsTest
作成したコードは次の通りです。ただし、MarubatsuConsoleTestは、標準出力の内容を取得して。。。とちょっと面倒なので、今回は実装しません。
<JUnit実行結果>
package jp.zenryoku.tutorial.level2;
import org.junit.jupiter.api.Test;
import java.util.Scanner;
import static org.hamcrest.core.Is.is;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.matchers.JUnitMatchers.either;
public class MarubatsuUtilsTest {
/** テスト対象クラス(全てstaticメソッドなのでインスタンス不要) */
private static MarubatsuUtils target;
@Test
public void testGetScanner() {
Scanner scan = MarubatsuUtils.getScanner();
// インスタンスが取得できていることを確認
assertNotNull(scan);
}
@Test
public void testNextInt() {
int res = MarubatsuUtils.nextInt(2);
// インスタンスが取得できていることを確認
boolean isRondom = res == 0 || res == 1;
assertTrue(isRondom);
}
@Test
public void testIsAtari() {
assertTrue(MarubatsuUtils.judgeAtariOrNot(0, 0));
assertTrue(MarubatsuUtils.judgeAtariOrNot(1, 1));
assertFalse(MarubatsuUtils.judgeAtariOrNot(0, 1));
assertFalse(MarubatsuUtils.judgeAtariOrNot(1, 0));
}
}
でわでわ。。。