先日、オブジェクト指向についての勉強で、以下のような課題が出された。
ビンゴゲームでビンゴか判定するコードを書け。
ビンゴカードは二次元配列とし、読み上げられた数字はリストとする。
私は以下のようにクラス分けして回答をしました。
- ビンゴカード
- 読み上げられた数字
- ビンゴ判定機能
public boolean bingoGame(int[][] bingoCardNums, List<Integer> pickedNums) {
// 選択されたナンバー
PickedNumbers numbers = new PickedNumbers(pickedNums);
// ビンゴカード
BingoCard card = new BingoCard(bingoCardNums);
// ビンゴ判定機能
BingoChecker checker = new BingoChecker(card, numbers);
// ビンゴ判定を実施
if (checker.isBingo()) {
return true;
} else {
return false;
}
}
class PickedNumbers {
private List<Integer> numbers;
public Numbers(List<Integer> numbers) {
this.numbers = numbers;
}
// 指定のナンバーを含むか判定
public boolean isContains(int chkNum) {
return this.numbers.contains(chkNum);
}
}
class BingoCard {
private int[][] cardNums;
public Card (int[][] numbers) throws IllegalArgumentException {
// エラーチェック
if (numbers.length == 0) {
throw new IllegalArgumentException("二次元データでない");
}
// エラーチェック
if (numbers.length == numbers[0].length) {
throw new IllegalArgumentException("縦と横の規模が不一致");
}
this.cardNums = numbers;
}
// ビンゴカードの規模を返却
public int getSize() {
return this.cardNums.length;
}
// 指定した垂直列を返却
public int[] getVerticalLineAt(int idx) throws IllegalArgumentException {
int size = getSize();
int[] returnLine = new int[size];
// エラーチェック
if (idx >= size) {
throw new IllegalArgumentException("インデックスが範囲外");
}
// 縦列を抽出
for (int i = 0; i < size; i++) {
returnLine[i] = this.cardNums[i][idx];
}
return returnLine;
}
// 指定した水平列を返却
public int[] getHorizonalLineAt(int idx) throws IllegalArgumentException {
int size = getSize();
// エラーチェック
if (idx >= size) {
throw new IllegalArgumentException("インデックスが範囲外");
}
return this.cardNums[idx];
}
// 右下斜め列を返却
public int[] getRightDownLine() {
int size = getSize();
int x = 0;
int y = 0;
int[] returnLine = new int[size];
while (y < size) {
returnLine[x] = this.cardNums[y][x];
x++;
y++;
}
return returnLine;
}
// 左下斜め列を返却
public int[] getLeftDownLine() {
int size = getSize();
int x = size;
int y = 0;
int[] returnLine = new int[size];
while (y < size) {
returnLine[x] = this.cardNums[y][x];
x--;
y++;
}
return returnLine;
}
}
class BingoChecker {
private Card card;
private Numbers numbers;
public BingoChecker(Card card, Numbers numbers) {
this.card = card;
this.numbers = numbers;
}
// ビンゴ判定
public boolean isBingo() {
int bingoSize = card.getSize();
// 縦チェック
for (int i = 0; i < bingoSize; i++) {
if (isBingoWithLine(this.card.getVerticalLineAt(i))) {
return true;
}
}
// 横チェック
for (int i = 0; i < bingoSize; i++) {
if (isBingoWithLine(this.card.getHorizonalLineAt(i))) {
return true;
}
}
// 右斜めチェック
if (isBingoWithLine(this.card.getRightDownLine())) {
return true;
}
// 左斜めチェック
if (isBingoWithLine(this.card.getLeftDownLine())) {
return true;
}
// ビンゴがない場合
return false;
}
// 引数列がビンゴか判定
private boolean isBingoWithLine(int[] lineNums) {
for (int num : lineNums) {
if (!this.numbers.isContains(num)) {
return false;
}
}
return true;
}
}
課題の「ビンゴか判定しろ」にしては少しやりすぎなコードかなと思いましたが、オブジェクト指向うんぬんの話だったので、スクリプト的なコードではなく上記のコードで回答しました。
クラス分けは本当に難しいですね。縦方向、横方法、斜め方向をx,y方向の加算値としてクラス化(Enumなど)するも考えたのですが、今回はしていません。
何をどうクラス分けするかは、今後の変更や拡張を予想し将来の修正作業に備える事だと思います。クラスとして表現できるものを全てクラス化すると、修正時にクラスが表現するモノは何か?を将来の修正者に読み解くことを要求してしまいます。
(特に抽象性が高いモノは読み解く難易度が高そうです。今回の場合は”方向”クラス。)
逆に手続き的に書くと、クラスが表現するモノを考えなくて済みます。なにせクラスを作成しませんから。また、クラス化するとは複数の箇所から使用されるため、共通化することと同義です。共通化したモノが本質的に同じモノかを考える必要があります。(単一責任原則)
仮に「方向クラス」を実装すると考えると、「ビンゴカード」、「ビンゴ判定機能」に利用される事になります。しかし、この2つのクラスに共通的に利用する機能を「方向クラス」に拡張するとは感じられませんでした。そのため「ビンゴカード」と「ビンゴ判定機能」の”方向”は「方向クラス」として共通化するべきでないと考えました。
この課題に特に回答がある訳でも無かったので、他の方ならどうクラス分けするのか聞いてみたいなーと。私は実践が不足しているため、経験者の方の意見をコメントなどで聞かせていただけるとありがたいです。
クラス分けは難しいですが、プログラマーの一番楽しい部分なのかなーと思います。
コメント