前回は導入から初期状態の盤面を取得するところまで。クラスはこんな感じになりました。
プレイヤーを含めて初期状態の盤面を取得する
前回の対応だけではリバーシというゲームを開始できる状態ではない。
プレイヤーなども含めて初期状態の盤面を取得できるように考えていきたい✋
モデルを抽出する
前回と同様に、Wiki & 英語版Wiki をみてモデルを抽出する。
- ゲーム: game
- 初期状態の盤面: start position
- オリジナル: original
- オセロ: othello
- プレイヤー: players
- 白番、黒番: side
- 白番: light
- 黒番: dark
- 石(32枚): disks
- 白番、黒番: side
- 盤: board
- 位置: pieces
- 横: horizontal (a - h)
- 縦: vertical (1 - 8)
- 石: disk
- 色: side
- 位置: pieces
- 初期状態の盤面: start position
宙ぶらりんだった初期状態の盤面はゲームに紐付ける。
プレイヤーはどの色なのか、自分の石を管理する。
プレイヤーが持つ石を盤に配置する。
。。。プレイヤーを含めて考えて見ると、以下のリファクタリング要素が出てきた💧
- 初期状態の盤面を original と othello にする。
- historical, modernって表現でも良いけれど歴史としてオリジナルとオセロが出てくるのでリネーム。
- 石の色(disk color)がプレイヤーの白番黒番を統一する。
- プレイヤーの白番黒番と石の色の定義は同じで良さそう。Sideで統一する。
というわけでIssueを起票してリファクタリングする。
最終的にこ以下のようになる見込み。
Issueを切って対応開始👍
- 対応ブランチ: start-game
テストケースを作成する
今回はプレイヤーの石を渡してボードに配置する必要があるから、BoardFacotry
にPlayers
を渡す必要がある。
BoardService.start(StartPosition)
はGameService.start(StartPosition)
で作り直した方が良さそうだ🤔
@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 未実装
というコメントを残して置くことで実装漏れを防ぐ。
public Game start(StartPosition startPosition) {
// TODO 未実装
return new Game();
}
テストケースが正しく動作するように実装する
サービス層には業務ロジックのみ
プレイヤーの初期状態を作成、それに合わせて 初期配置の状態
に合わせて盤を作成し、それを返す。
ということで、必要のない情報を削ぎ落とすと以下になった。
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
がおすすめ。
(ラムダ式については読みづらくならないように注意が必要)
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;
}
以下とどちらが文章として読みやすいかを考えて選択していく🤔
(私の場合はなるべく不等号とか括弧は減らした方が読みやすいと思って〼✋)
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;
}
※ 変数を省略(index
→i
)しない。省略して可読性が上がることなんてほぼない🙅♂️
プレイヤーの石を盤に置く
プレイヤーの石を取って、盤に配置している箇所。
Playerに対して takeDisk() を呼ぶことで先頭の石をリストから外し、外した石を盤に配置している。
(普段はRDBを利用しているからかList.remove(index)
って中々使わないような。。)
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())
);
}
public Disk takeDisk() {
return disks.remove(0);
}
リファクタリング
パッケージ構成がぐちゃぐちゃになっていたので整理。
- board.StartPosition → game.StartPosition
- piece.Piece → board.piece.Piece
- disk.Disk → player.disk.Disk
- disk.Side → player.Side
※ コミットする単位は、対応する内容に応じてコミットを分けるべき。
特にリファクタリングはその対応が問題ないのか細分化した状態でレビューしてもらうべき。
(今回、リファクタリングのコミットを一括でやってしまったので反省。。。💧)
最後に
今回はここまで。
TDD方式で開発すると実際に動くアウトプットがなくても、テストによる動作確認ができるのが良いなと思いました🤔
あとは最小単位での開発(アジャイル開発)は成果物が常に発生するのでモチベーションに繋がりますね👍
TDD方式でリファクタリングする機会も多くなり品質もよりよくなってる実感あります。
TDD方式の開発がもっと流行れば良いなぁ。。。