0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Reversi をつくろう! - 2

Last updated at Posted at 2021-04-04

< 前回の記事

前回は導入から初期状態の盤面を取得するところまで。クラスはこんな感じになりました。
refactored_class.png

プレイヤーを含めて初期状態の盤面を取得する

前回の対応だけではリバーシというゲームを開始できる状態ではない。
プレイヤーなども含めて初期状態の盤面を取得できるように考えていきたい✋

モデルを抽出する

前回と同様に、Wiki & 英語版Wiki をみてモデルを抽出する。

  • ゲーム: game
    • 初期状態の盤面: start position
      • オリジナル: original
      • オセロ: othello
    • プレイヤー: players
      • 白番、黒番: side
        • 白番: light
        • 黒番: dark
      • 石(32枚): disks
    • 盤: board
      • 位置: pieces
        • 横: horizontal (a - h)
        • 縦: vertical (1 - 8)
        • 石: disk
          • 色: side

宙ぶらりんだった初期状態の盤面はゲームに紐付ける。
プレイヤーはどの色なのか、自分の石を管理する。
プレイヤーが持つ石を盤に配置する。

。。。プレイヤーを含めて考えて見ると、以下のリファクタリング要素が出てきた💧

  • 初期状態の盤面を original と othello にする。
    • historical, modernって表現でも良いけれど歴史としてオリジナルとオセロが出てくるのでリネーム。
  • 石の色(disk color)がプレイヤーの白番黒番を統一する。
    • プレイヤーの白番黒番と石の色の定義は同じで良さそう。Sideで統一する。

というわけでIssueを起票してリファクタリングする。

最終的にこ以下のようになる見込み。

image.png

Issueを切って対応開始👍

テストケースを作成する

今回はプレイヤーの石を渡してボードに配置する必要があるから、BoardFacotryPlayersを渡す必要がある。
BoardService.start(StartPosition)GameService.start(StartPosition)で作り直した方が良さそうだ🤔

GameServiceTest.java
    @Autowired
    GameService service;

    Stream<Arguments> 初期状態の盤面一覧() {
        return Stream.of(arguments(StartPosition.original,
                                   32,
                                   32,
                                   "[]"),
                         arguments(StartPosition.othello,
                                   30,
                                   30,
                                   "[{light: (d, 4)}, {dark: (e, 4)}, {dark: (d, 5)}, {light: (e, 5)}]")
        );
    }

    @ParameterizedTest
    @MethodSource("初期状態の盤面一覧")
    public void 初期状態の盤面を取得する(
            StartPosition startPosition,
            int 期待する白番の石数,
            int 期待する黒番の石数,
            String 期待する盤面) {
        Game game = service.start(startPosition);

        assertThat(game.lightPlayer().diskCount(), is(期待する白番の石数));
        assertThat(game.darkPlayer().diskCount(), is(期待する黒番の石数));
        assertThat(game.board().toString(), is(期待する盤面));
    }
  • 初期配置の状態がオリジナルだと盤はそのまま。プレイヤーの石数は初期状態である32個。
  • 初期配置の状態がオセロだと盤にプレイヤーの石をセンターエリアに2つずつ配置する。石数は30個。

というテストケースを作成。
BoardServiceに関する実装はこのタイミングで削除(リファクタリングのタイミングで本来は削除。。)

実装部分は以下のようにTODO 未実装というコメントを残して置くことで実装漏れを防ぐ。

GameService.java
    public Game start(StartPosition startPosition) {
        // TODO 未実装
        return new Game();
    }

テストケースが正しく動作するように実装する

サービス層には業務ロジックのみ

プレイヤーの初期状態を作成、それに合わせて 初期配置の状態 に合わせて盤を作成し、それを返す。
ということで、必要のない情報を削ぎ落とすと以下になった。

GameService.java
    public Game start(StartPosition startPosition) {
        final Players players = playersFactory.create();
        final Board board = boardFactory.create(startPosition, players);

        return new Game(startPosition,
                        players,
                        board);
    }

プレイヤーの石32個の取得方法について

プレイヤーの石数32個という固定値はPlayerクラスに定数を作成して管理する。
そして、単純な回数分の繰り返しはラムダ式のIntStream.rangeClosedがおすすめ。
(ラムダ式については読みづらくならないように注意が必要)

PlayersFactory.java
    private List<Disk> createDisks(Side side) {
        final List<Disk> disks = new ArrayList<>();
        IntStream.rangeClosed(1, Player.MAX_DISK_COUNT)
                 .forEach(count -> disks.add(createDisk(side)));
        return disks;
    }

以下とどちらが文章として読みやすいかを考えて選択していく🤔
(私の場合はなるべく不等号とか括弧は減らした方が読みやすいと思って〼✋)

PlayersFactory.java
    private List<Disk> createDisks(Side side) {
        final List<Disk> disks = new ArrayList<>();
        for(int index=0; index<Player.MAX_DISK_COUNT; index++) {
            disks.add(createDisk(side))
        }
        return disks;
    }

※ 変数を省略(indexi)しない。省略して可読性が上がることなんてほぼない🙅‍♂️

プレイヤーの石を盤に置く

プレイヤーの石を取って、盤に配置している箇所。
Playerに対して takeDisk() を呼ぶことで先頭の石をリストから外し、外した石を盤に配置している。
(普段はRDBを利用しているからかList.remove(index)って中々使わないような。。)

BoardFactory.java
    private List<Piece> diagonalPieces(Players players) {
        return List.of(
                createPiece(4, 4, players.light().takeDisk()),
                createPiece(5, 4, players.dark().takeDisk()),
                createPiece(4, 5, players.dark().takeDisk()),
                createPiece(5, 5, players.light().takeDisk())
        );
    }
Player.java
    public Disk takeDisk() {
        return disks.remove(0);
    }

GameServieに関わるクラスは以下となった。
image.png

リファクタリング

パッケージ構成がぐちゃぐちゃになっていたので整理。

  • board.StartPosition → game.StartPosition
  • piece.Piece → board.piece.Piece
  • disk.Disk → player.disk.Disk
  • disk.Side → player.Side

※ コミットする単位は、対応する内容に応じてコミットを分けるべき。
特にリファクタリングはその対応が問題ないのか細分化した状態でレビューしてもらうべき。
(今回、リファクタリングのコミットを一括でやってしまったので反省。。。💧)

最後に

今回はここまで。
TDD方式で開発すると実際に動くアウトプットがなくても、テストによる動作確認ができるのが良いなと思いました🤔
あとは最小単位での開発(アジャイル開発)は成果物が常に発生するのでモチベーションに繋がりますね👍
TDD方式でリファクタリングする機会も多くなり品質もよりよくなってる実感あります。

TDD方式の開発がもっと流行れば良いなぁ。。。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?