블록을 해당하는 위치에 놓을 수 있는지 확인하는 함수를 작성해보자
클린 코드에서는 "코드를 깔끔하게 정리하고 표현력을 강화하는 방향으로, 그래서 애초에 주석이 필요 없는 방향으로 에너지를 쏟겠다"라고 말한다(p.69). "함수나 변수로 추가할 수 있다면 주석을 달지 마라"는 저자의 메세지와 같이 주석이 필요 없는 코드를 위해 고민해보자. 오늘은 놓을_수_있는지_확인()
부분을 작성해보겠다.
수정해야 할 부분이 있다. 바로 블록을 "누가 놓는지" 알 수 없는 파라미터 구조이다. 간단하게 플레이어가 몇 번째 차례인지 확인하도록 인덱스만 넘겨받도록 해보자.
export type BlockMatrix = (number | false)[][];
export interface PlaceBlockDTO {
block: BlockMatrix;
position: number[];
board: BoardMatrix;
playerIdx: 0 | 1 | 2 | 3;
}
export interface PutBlockDTO {
board: BoardMatrix;
blockInfo: Block;
position: number[];
playerIdx: 0 | 1 | 2 | 3;
}
플레이어 인덱스가 턴 정보와 일치하는지, 요청자가 해당 인덱스를 가진 플레이어가 맞는지 체크가 된 상태에서
Blokus의 게임 룰은 다음과 같다.
[0,0]
, [19,0]
, [0,19]
, [19,19]
)에 배치되어야 함1은 턴으로 구현되어야 하기 때문에 해당 함수에서 검사하는 것이 아닌 플레이어들이 요청을 보낼 때 확인하도록 한다. 2~4를 한 번에 검사하면 가독성이 박살난다는 것을 상식과 감(과 경험)으로 알기 때문에 이를 조금 분리해보려고 한다. 일단 이 모든 과정을 수행하는 함수를 다음과 같이 작성해보자.
export const isBlockPlaceable = ({ block, position, board, playerIdx }: PutBlockDTO) => {
// 2
// 2-1
// 3
// 3-1
// 4
};
블록이 보드를 벗어나지 않는지, 첫 번째 턴에 모서리에 잘 뒀는지 확인을 해야 한다. 이 작업을 각각 아래 함수로 작성해보자.
const isWithinBoardBounds = ... ;
const isFirstMoveValid = ... ;
코딩할 때 가장 어려운 것 중 하나인 네이밍을 했으니 내부를 아름답게 구현해보자.
일단 isWithinBoardBounds
함수에 대해 생각해보자.
position이 in-range인데 true인 블록 셀이 보드를 벗어날 수 있는가?
즉,
position
이 유효한가(in-range인가)?1을 만족하고 2를 어길 수가 있을까? 모든 블록과 모서리를 고려해봐도 없는 것 같다. 즉 isWithinBoardBounds
함수에서는 position
과 블록의 높이와 너비를 체크하면 될 것 같다.
const [row, col] = position;
const boardHeight = 20;
const boardWidth = 20;
const blockHeight = block.length;
const blockWidth = block[0].length;
return !(
row < 0 ||
row + blockHeight > boardHeight ||
col < 0 ||
col + blockWidth > boardWidth
);
가독성을 위해 row
, col
등의 '이름에 의미가 담긴' 변수들을 사용해봤다.
코너에 올라갈 블록의 셀이 true일까?
내가 생각하는 해법은 두 가지이다. 첫째는 turn과 관련된 파라미터를 전달받는 것이고 둘째는 모서리를 체크하는 것이다. 나는 두 번째 방식을 해답으로 택하고 싶었다. 즉 보드의 모서리가 비어있으면 해당 플레이어는 첫 턴이라고 판정하고 싶었다.
혈압이 오른 로버트 마틴 씨가 이의를 제기한다. 결합성 관점에서 모서리를 체크하는 방식이 왜 나쁜지 알아보자.
if (board[0][0] === false || board[0][19] || board[19][0] || board[19][19]) {
// [TODO] check whether the block's cell is on the edge of the board or not
}
이렇게 구현 시 해당 함수는 board
의 구현에 의존하게 된다. 만일 board
의 구현을 아래와 같이 수정한다고 가정해보자.
const board = {
0: [ /*first line of the board*/],
1: [ /*second line of the board*/],
...
};
board에 생긴 수정사항으로 인해 validation을 담당하는 함수들까지 수정해야 하는 끔찍한 일이 발생한다.
// after
if (board.0[0] === false || ...)
고수준 모듈(에 가까운) 블록 배치 검증 로직이 저수준 모듈(에 가까운) board
의 구체적 구현에 의존하게 되기 때문에 의존성 역전 법칙
을 위배한다고 볼 수 있다.
하지만, 모든 함수 구현을 보드나 블록 등의 구현 방식에 의존하지 않도록 SOLID 원칙에 맞춰 구현하는 방법이 나에게는 떠오르지 않는다. 더 atomic하게 구성하는 방법은 실력이 더 쌓이고 고민해보도록 하겠다.
일단 가장 상위 함수인 putBlockOnBoard
에서 turn 정보를 받아와서 체크하도록 해보자.
// $lib/types.ts
export interface PutBlockDTO {
turn: number;
...
export interface PlaceBlockDTO {
turn: number;
...
// $lib/game.ts
const isFirstMoveValid = ({ block, position, board, playerIdx, turn }: PlaceBlockDTO) => {
if (turn > 3) {
return;
}
// [TODO] check the block is on the edge of the board
};
어떻게 체크해야 할까? 우선 나는 각 플레이어의 턴이 clockwise로 진행된다고 확정하겠다. 좌상단-우상단-우하단-좌하단 순으로 게임을 진행한다고 가정했을 때, playerIdx
가 0이면 (0,0), 1이면 (0,19), 2면 (19, 19), 3이면 (19, 0)에 true인 셀이 위치해야 한다. 셀이 위치해야 함은, 블록의 모서리도 true여야 하며 이 모서리 셀이 보드의 모서리에 위치해야 한다. 다시 말해 이를 따로 검사하는 것보다 프리셋을 사용해 한 번에 검사하는 것이 나을 것이다. 메모리 공간을 조금 더 활용해 if 분기를 많이 줄일 수 있다.
export const isFirstMoveValid = ({ board, block, playerIdx, turn, position }: PlaceBlockDTO) => {
if (turn > 3) {
return true;
}
const [row, col] = position;
const blockHeight = block.length;
const blockWidth = block[0].length;
const cornerPositionsPreset = {
0: { board: [0, 0], block: [0, 0] },
1: { board: [0, 19], block: [0, blockWidth - 1] },
2: { board: [19, 19], block: [blockHeight - 1, blockWidth - 1] },
3: { board: [19, 0], block: [blockHeight - 1, 0] },
};
const { board: boardPosition, block: blockPosition } = cornerPositionsPreset[playerIdx];
if (!block[blockPosition[0]][blockPosition[1]]) {
return false;
}
return row + blockHeight - 1 === boardPosition[0]
&& col + blockWidth - 1 === boardPosition[1];
};
export const isBlockPlaceable = ({ block, position, board, playerIdx, turn }: PlaceBlockDTO) => {
if (!isWithinBoardBounds({ block, position, board, playerIdx, turn })) {
return false;
}
if (!isFirstMoveValid({ block, position, board, playerIdx, turn })) {
return false;
}
...
3, 3-1, 4에 해당하는 validation은 다음 글에 이어서 작성해보겠다. 해당 글에서 다뤘던 부분을 이렇게 작성해버렸던 21개월 전의 나를 반성해보는 시간을 가지며 글을 마무리하겠다.