LoginSignup
12
5

More than 3 years have passed since last update.

「ビンゴ判定プログラム」をリファクタリングさせていただいた

Last updated at Posted at 2017-04-12

@johejoさんの「ビンゴ判定プログラム」の投稿を拝見して、面白いネタだなと思いつつも、すべて staticメソッドな手続き型プログラムになっているのが残念に思えました。
せっかくのJavaですから、オブジェクト指向でリファクタリングしてみます。
もちろん、これが最終形ではありません。より良いリファクタリングができると思いますので、みなさんも試行錯誤してみてください。

リファクタリング3回目

再び @k73i55no5 さんにコメントをいただき、Bingoクラスだけリファクタリングしました。

クラス構成概要

  • BingoクラスはUI(データ入力、メイン処理、結果出力)
  • SelectableNumberはカード上の数字
  • SelectableNumbersは 5x5=25個のSelectableNumberを保持
  • BingoLineは縦横斜めに並んでいるライン
  • ひとつのBingoLineは5個のSelectableNumberを保持
  • 複数のBingoLineでひとつのSelectableNumberを参照共有
  • BingoLinesは5行+5列+2斜め=12ラインのBingoLineを保持
  • SelectableNumberは数字が選択されたことを保持する
  • SelectableNumberが選択されたことは参照しているBingoLineから把握できる
  • BingoLineは5個のSelectableNumberを調べてリーチかビンゴか判定できる
  • BingoLinesは12個のBingoLineを調べてリーチとビンゴの数を数えられる

クラス図

image.png

コード(Bingoクラスのみ)

public class Bingo {
    public static void main(String[] args) throws Exception {
        Bingo bingo = new Bingo(loadCardNumbers("board.txt"));
        bingo.select(loadSelectionNumbers("selected.txt"));
        bingo.show();
    }

    public static int[][] loadCardNumbers(String filename) throws IOException {
        return Files.readAllLines(Paths.get(filename))
                    .stream()
                    .map(line -> Arrays.stream(line.split(" "))
                                       .mapToInt(Integer::parseInt)
                                       .toArray())
                    .toArray(int[][]::new);
    }

    public static int[] loadSelectionNumbers(String filename) throws IOException {
        return Files.readAllLines(Paths.get(filename))
                    .stream()
                    .mapToInt(Integer::parseInt)
                    .toArray();
    }

    private final BingoCard card;

    public Bingo(int[][] numbers) {
        this(new BingoCard(numbers));
    }

    Bingo(BingoCard card) {
        this.card = card;
    }

    public void select(int[] numbers) {
        Arrays.stream(numbers).forEach(card::select);
    }

    public void show() {
        System.out.println("BINGO:" + card.countBingo());
        System.out.println("REACH:" + card.countReach());
    }
}

リファクタリング2回目

※ 1回目の内容は記事後半にあります。

@k73i55no5 さんにコメントしていただいた点を踏まえ、2回めのリファクタリングを実施しました。
ビンゴカードのマスは 5x5 固定、データサイズの整合性チェックなどは省略しています。
更なる改善点などありましたらコメントをお願いします。

コード

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Stream;

public class Bingo {
    public static void main(String[] args) throws Exception {
        // ビンゴカードの数字データを読み込み、ビンゴカードを作成する
        int[][] numbers = Files.readAllLines(Paths.get("board.txt"))
                               .stream()
                               .map(line -> Arrays.stream(line.split(" "))
                                                  .mapToInt(Integer::parseInt)
                                                  .toArray())
                               .toArray(int[][]::new);
        BingoCard card = new BingoCard(numbers);
        // ビンゴ大会主催者が選んだ数字列を読み込みビンゴカードに反映する
        Files.readAllLines(Paths.get("selected.txt"))
             .stream()
             .map(Integer::parseInt)
             .forEach(card::select);
        // ビンゴカードの状態を表示する
        System.out.println("BINGO:" + card.countBingo());
        System.out.println("REACH:" + card.countReach());
    }
}

class BingoCard {
    private final SelectableNumbers numbers;
    private final BingoLines lines;

    public BingoCard(int[][] numbers) {
        this.numbers = new SelectableNumbers(numbers);
        this.lines = new BingoLines(this.numbers);
    }

    public void select(int number) {
        numbers.select(number);
    }

    public long countReach() {
        return lines.countReach();
    }

    public long countBingo() {
        return lines.countBingo();
    }
}

class SelectableNumber {
    public final int number;
    private boolean selected = false;

    public SelectableNumber(int number) {
        this.number = number;
    }

    public void select() {
        selected = true;
    }

    public boolean isSelected() {
        return selected;
    }
}

class SelectableNumbers {
    private final SelectableNumber[] numbers;

    private static SelectableNumber[] flattenNumbers(int[][] numbers) {
        return Stream.of(numbers)
                     .flatMapToInt(Arrays::stream)
                     .mapToObj(SelectableNumber::new)
                     .toArray(SelectableNumber[]::new);
    }

    public SelectableNumbers(int[][] numbers) {
        this.numbers = flattenNumbers(numbers);
    }

    public SelectableNumber getAt(int index) {
        return numbers[index];
    }

    public void select(int number) {
        Stream.of(numbers)
              .filter(n -> n.number == number)
              .forEach(n -> n.select());
    }
}

class BingoLine {
    private final SelectableNumber[] numbers;

    public BingoLine(SelectableNumber[] numbers) {
        this.numbers = numbers;
    }

    private long countSelectedNumbers() {
        return Stream.of(numbers).filter(number -> number.isSelected()).count();
    }

    public boolean isReach() {
        return countSelectedNumbers() == numbers.length - 1;
    }

    public boolean isBingo() {
        return countSelectedNumbers() == numbers.length;
    }
}

class BingoLines {
    // ラインのインデックス番号配列、インデックス番号は0〜24決め打ち
    private static final int[][] LINES = new int[][] {
        //    横のライン          縦のライン
        { 0,  1,  2,  3,  4}, {0, 5, 10, 15, 20},
        { 5,  6,  7,  8,  9}, {1, 6, 11, 16, 21},
        {10, 11, 12, 13, 14}, {2, 7, 12, 17, 22},
        {15, 16, 17, 18, 19}, {3, 8, 13, 18, 23},
        {20, 21, 21, 23, 24}, {4, 9, 14, 19, 24},
        //    左上から左下        右上から左下  の斜めライン
        { 0,  6, 12, 18, 19}, {4, 8, 12, 16, 20},
    };

    private static BingoLine[] makeBingoLines(SelectableNumbers numbers) {
        return Stream.of(LINES)
                     .map(line -> Arrays.stream(line)
                                        .mapToObj(numbers::getAt)
                                        .toArray(SelectableNumber[]::new))
                     .map(BingoLine::new)
                     .toArray(BingoLine[]::new);
    }

    private final BingoLine[] lines;

    public BingoLines(SelectableNumbers numbers) {
        lines = makeBingoLines(numbers);
    }

    public long countReach() {
        return Stream.of(lines).filter(line -> line.isReach()).count();        
    }

    public long countBingo() {
        return Stream.of(lines).filter(line -> line.isBingo()).count();        
    }
}

リファクタリング1回目

ビンゴカードには5行5列のランダムな数字が書かれています。厳密には列毎に数字の範囲が決まっているのですが、それは気にしないことにします。
ビンゴ大会では、各人に並びが異なる数字が書かれた紙のカードが配られ、主催者が数字の書かれたボールをランダムに選び、その数字がカードの中にあれば、その数字の部分を折り曲げて選ばれた状態にします。
カードには、行、列、斜めの並びがあって、ひとつの並びには5つの数字が含まれます。
ひとつの並びの中のすべての数字が選ばれると「ビンゴ」となり景品がもらえたりします。あとひとつ選ばれるとビンゴになる状態を「リーチ」と呼びます。

「カード」、「数字」、「並び」という言葉が出てきましたので、これを Card, Number, Line クラスにします。
ビンゴ大会主催者が数字を選ぶと、普段は自分で手持ちのビンゴカードの中から数字を探して紙を折って選んだ状態にしますが、オブジェクト指向の世界ではクラスを擬人化して使うことができます。
人: なぁなぁカード君、この数字あるかな? あったら選んどいて。
カード君: お、あったわ。数字君、君選ばれたで。
数字君: おっ、俺選ばれたんか。属してる並びに教えたろ。
並び君: おお、選ばれた数字がひとつ増えた。カード君、僕に並んでる数字の選ばれた数が4個になりましたで。
カード君: お、4つ揃ったんか、リーチやないか。あとひとつ早よこんかい。

そんな様子をプログラムにしてみました。

コード

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Stream;

// ビンゴカード
class Card {

    // ビンゴカード上の数字
    class Number {

        final int value;
        private boolean isSelected = false;
        private final List<Line> lines = new ArrayList<>();

        Number(int value) {
            this.value = value;
        }

        // 縦、横、斜めの並びに属する
        void belong(Line line) {
            lines.add(line);
        }

        // 数字が選ばれた
        void selected() {
            if (!isSelected) {
                isSelected = true;
                // 属する並びに知らせる
                lines.stream().forEach(line -> line.selected());
            }
        }
    }

    // ビンゴカード上の並び(縦、横、斜め)
    class Line {

        private int selectedCount = 0;

        // 並びの中の数字が選ばれた
        void selected() {
            selectedCount++;
            // カードの状態を更新する
            judge(this);
        }
    }

    final int size;
    int bingo = 0, reach = 0;
    private final HashMap<Integer, Number> myNumbers = new HashMap<>();

    // ビンゴカード生成
    Card(int[][] numbers) {
        // 縦、横、斜めの並びを作る
        size = numbers.length;
        Line[] rowLines = new Line[size];   // 横の並び
        Line[] colLines = new Line[size];   // 縦の並び
        Line backslashLine = new Line();    // 左上から右下への斜めの並び
        Line slashLine = new Line();        // 右上から左下への斜めの並び
        for (int i = 0; i < size; i++) {    // 配列の中身は空っぽなので初期化
            rowLines[i] = new Line();
            colLines[i] = new Line();
        }
        // 各数字を作り、並びに属させる
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                Number number = new Number(numbers[row][col]);
                number.belong(rowLines[row]);
                number.belong(colLines[col]);
                if (row == col) {
                    number.belong(backslashLine);
                }
                if (row == size - col - 1) {
                    number.belong(slashLine);
                }
                myNumbers.put(number.value, number);
            }
        }
    }

    // 数字があれば選んだ状態にする
    void selected(int number) {
        Number myNumber = myNumbers.get(number);
        if (myNumber != null) {
            myNumber.selected();
        }
    }

    // 並び内の選ばれている数字の数により、リーチとビンゴの数を更新する
    void judge(Line line) {
        if (line.selectedCount == size - 1) {
            reach++;
        } else if (line.selectedCount == size) {
            reach--;    // リーチ時に数えているので減らす
            bingo++;
        }
    }
}

// ビンゴ大会
public class Bingo {

    public static void main(String[] args) throws Exception {
        // ビンゴカードの数字データを読み込み、ビンゴカードを作成する
        List<String> board = Files.readAllLines(Paths.get("board.txt"));
        int size = board.size();
        int[][] numbers = new int[size][size];
        for (int row = 0; row < size; row++) {
            String[] values = board.get(row).split(" ");
            for (int col = 0; col < size; col++) {
                numbers[row][col] = Integer.parseInt(values[col]);
            }
        }
        Card card = new Card(numbers);
        // ビンゴ大会主催者が選んだ数字列を読み込みビンゴカードに反映する
        Stream<String> selected = Files.lines(Paths.get("selected.txt"));
        selected.forEach(number -> card.selected(Integer.parseInt(number)));
        // ビンゴカードの状態を表示する
        System.out.println("BINGO:" + card.bingo + "\nREACH:" + card.reach);
    }
}

入力ファイル

biard.txt
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
selected.txt
1
7
13
19
2
3
4
12
5
15

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5