2편에서는...
2편에서는 본격적으로 구현을 시작하였다. 도메인 모델을 클래스 다이어그램으로 구체화 하여 실제 코드로 구현하였다. 다음은 2편에서까지 완성했던 클래스 다이어그램이다.
3편에서는
Board와 Tile객체 그리고 각 체커 객체를 더욱 구체화하여 코드를 완성했다.
Tile 2중리스트로 구현한 Board
현실에서의 보드와 타일블록
현실에서는 격자모양의 보드에 정사각형의 타일블록을 올려놓는 형태가 될것이다. 그렇다면 쉽게 생각할 수 있는 것이 Board는 Tile객체 타입의 이중리스트를 갖고, 거기에 Tile객체를 저장하는 것이다. 글쓴이도 처음엔 이 생각을 가지고 구현을 했지만 다음과 같은 문제가 발생했다.
- 어떤 순서로 Tile이 놓아졌는지 저장할 수 없음.
- 타일과 철로의 연결성을 검사할 때 비효율적임.
사실 첫번째 문제는 Tile을 보드에 놓을때(push) 스택을 생성해 스택에 푸쉬해주면 쉽게 해결할 수 있다. 치명적인 문제는 두번째 문제이다. 해당 프로그램에서는 플레이어 객체가 타일을 보드에 푸쉬해주면 체커 객체가 문제가 없는지 검사를 한다. 이 과정에서 타일이 기존 타일과 연결되었는지, 타일에 그려진 철로가 기존 철로와 연결되었는지 조사하는데 데이터를 위와 같이 저장해 놓으면 검사 코드가 가독성이 떨어지고 비효율적이다.
실제 작성한 코드 Board.java
public boolean isRailConnect() {
int posX = tileLog.peek().getX();
int posY = tileLog.peek().getY();
if (posX+1 < 17 &&
board[posX+1][posY].getUp() != Tile.Connect.DEFAULT &&
board[posX+1][posY].getUp() != board[posX][posY].getDown()) return false;
if (posX-1 >= 0 &&
board[posX-1][posY].getDown() != Tile.Connect.DEFAULT &&
board[posX-1][posY].getDown() != board[posX][posY].getUp()) return false;
if (posY+1 < 17 &&
board[posX][posY+1].getLeft() != Tile.Connect.DEFAULT &&
board[posX][posY+1].getLeft() != board[posX][posY].getRight()) return false;
if (posY-1 >= 0 &&
board[posX][posY-1].getRight() != Tile.Connect.DEFAULT &&
board[posX][posY-1].getRight() != board[posX][posY-1].getLeft()) return false;
return true;
}
해당 코드는 Board 객체의 가장 최근의 놓은 선로의 연결성을 검사하는 메서드이다. 보다시피 연결성을 조사하기위해 주변 4개의 타일을 가져와야 하고, 가져온 타일마다 확인해야하는 방향이 달라 각각 다르게 구현해야한다. 또한 이중리스트 특성상 항상 경계값 예외를 처리해줘야 한다는 불편함도 있다.
또한 체커(Checker)에서 이 과정을 수행하려면 보드를 통째로 가져와야 하는데 이는 객체지향적 관점에서 좋지 못하다. 그러다 보니 Checker 객체가 해야하는 일을 Board 객체가 모두 다 수행하게 된다. 이로 인해 Checker 객체의 자율성이 심하게 저해된다.
중요한 것은 타일이 아니라 타일과 타일이 만나는 '선'이다.
우리가 필요한것은 타일블록과 보드 그 자체가 아니다. 결국 플레이어 객체가 푸쉬하고, 체커 객체가 검사하기 위해 필요한 데이터이다. 즉 '행동'이 먼저고 그 뒤에 '데이터'가 붙는 것이다.
연결성을 검사하는 체커 객체에게 중요한 것은 '타일' 그 자체가 아니다. 타일과 타일이 만나는 '선'이다. 그 선이 철로로 연결되었는지, 철로와 벽이 만났는지, 한쪽이 아직 타일이 놓여지지 않았는지 이런 것들이 더 중요한 데이터이다. 즉, 보드 객체는 타일이 아니라 타일과 타일 사이이의 경계선의 상태를 갖고 있어야 하며 그 데이터를 체커 객체에게 제공할 책임이 있다.
package board;
public class TileState {
public static enum Connect { DEFAULT, ABLED, DISABLED, COMPLETE, ERROR };
public static Connect pushState(Connect curr, int signal) {
Connect ret;
switch (curr) {
case DEFAULT :
ret = (signal == 1) ? Connect.ABLED : Connect.DISABLED;
break;
case ABLED :
ret = (signal == 1) ? Connect.COMPLETE : Connect.ERROR;
break;
case DISABLED :
ret = (signal == 1) ? Connect.ERROR : Connect.COMPLETE;
break;
default:
ret = Connect.ERROR;
}
return ret;
}
public static Connect popState(Connect curr, int signal) {
Connect ret;
switch (curr) {
case COMPLETE :
ret = (signal == 1) ? Connect.ABLED : Connect.DISABLED;
break;
case ERROR :
ret = (signal == 1) ? Connect.DISABLED : Connect.ABLED;
break;
case ABLED :
ret = (signal == 1) ? Connect.DEFAULT : Connect.ERROR;
break;
case DISABLED :
ret = (signal == 1) ? Connect.ERROR : Connect.DISABLED;
break;
default :
ret = Connect.ERROR;
}
return ret;
}
}
package board;
import java.util.Stack;
import java.util.*;
// singleton
public class Board {
private static Board instance = null;
private TileState.Connect horizontalLine[][] = new TileState.Connect[18][18];
private TileState.Connect verticalLine[][] = new TileState.Connect[18][18];
private Stack<Tile> tileLog = new Stack<>();
private int restTile = 16;
private int useTile;
private Board() {
for (int i=0; i<18; ++i) {
for (int j=0; j<18; ++j) {
horizontalLine[i][j] = TileState.Connect.DEFAULT;
verticalLine[i][j] = TileState.Connect.DEFAULT;
}
}
}
public static Board getInstance() {
if ( instance == null) {
return new Board();
}
return instance;
}
// pop and push tile
public void pushTile(int x, int y, List<Integer> type) {
tileLog.push(new Tile(x, y, type));
// up
TileState.pushState(horizontalLine[x][y], type.get(0));
// down
TileState.pushState(horizontalLine[x+1][y], type.get(1));
// left
TileState.pushState(verticalLine[x][y], type.get(2));
// right
TileState.pushState(verticalLine[x][y+1], type.get(3));
useTile++; restTile--;
}
public void popTile() {
int x = tileLog.peek().getX();
int y = tileLog.peek().getY();
List<Integer> type = tileLog.peek().getType();
tileLog.pop();
// up
TileState.popState(horizontalLine[x][y], type.get(0));
// down
TileState.popState(horizontalLine[x+1][y], type.get(1));
// left
TileState.popState(verticalLine[x][y], type.get(2));
// right
TileState.popState(verticalLine[x][y+1], type.get(3));
restTile++; useTile--;
}
// useTile checking
public int getRestTile() { return this.restTile; }
public int getUseTile() { return this.useTile; }
public void initUseTile() { this.useTile = 0; }
// connecting checking
public List<TileState.Connect> getAdjacnetState() {
List<TileState.Connect> ret = new ArrayList<>();
int x = tileLog.peek().getX();
int y = tileLog.peek().getY();
ret.add(horizontalLine[x][y]);
ret.add(horizontalLine[x+1][y]);
ret.add(verticalLine[x][y]);
ret.add(verticalLine[x][y+1]);
return ret;
}
public List<TileState.Connect> getAdjacnetState(int x, int y) {
List<TileState.Connect> ret = new ArrayList<>();
ret.add(horizontalLine[x][y]);
ret.add(horizontalLine[x+1][y]);
ret.add(verticalLine[x][y]);
ret.add(verticalLine[x][y+1]);
return ret;
}
}
horizontalLine, verticalLine 필드는 위에서 말한 타일과 타일의 경계들이다. TileState.Connect 이라는 정적 열거 타입을 정의해서 타일이 삽입되거나 제거 될때 마다 경계값은 변화한다. 또한 다른 객체에서 타일의 경계의 상태에 대한 데이터를 요청하면 Board 객체는 getAdjacnetState 메서드를 통해 해당 데이터를 제공한다. 즉, 보드의 상태를 관리하고, 그 데이터를 제공하는 보드의 역할에 충실할 수 있는 것이다.
RailConnectChecker.java
package checker;
import system.System;
import board.*;
import java.util.*;
public class RailConnectChecker extends Checker {
@Override
public System.State check() {
List<TileState.Connect> connectState = Board.getInstance().getAdjacnetState();
if (connectState.stream().filter( state -> state == TileState.Connect.ERROR ).count() > 0) {
return System.State.RAIL_CONNECT_ERROR;
}
return System.State.NONE;
}
}
이어서 Checker 인터페이스의 구현 객체인 RailConnectChecker를 보자. 이 코드는 철로의 연결성을 조사하는 코드이다. check() 메서드 블럭을 보면 일단 Board 객체에게 타일의 인접 경계선의 상태를 가져올 것을 요청한다(getAdjacentState) 그후 인접한 경계선들의 상태 중 TilState.Connect.Error(잘못된 연결)인게 있으면 RAIL_CONNECT_ERROR를 리턴한다.
즉, 체커 객체가 자신의 역할인 말그대로 '검사'를 할 수 있게 되는 것이다.
소프트웨어의 세계를 현실 세계와 그대로 만드는 것은 큰 의미가 없다. 오히려 그 방법론이 프로그램의 품질을 떨어뜨리기도 한다. (위와 같은 상황이 그렇다.) 실제로는 소프트웨어가 어떤식으로 작동하는지 이해하고 현실의 표현은 '은유' 하여 소프트웨어에 적용하려는 노력이 필요하다.
그외 코드들
package checker;
import system.System;
import board.*;
public class TileConnectChecker extends Checker {
@Override
public System.State check() {
if (Board.getInstance().getAdjacnetState()
.stream()
.filter( s -> (s == TileState.Connect.COMPLETE || s == TileState.Connect.ERROR) )
.count() == 0) {
return System.State.TILE_CONNECT_ERROR;
}
return System.State.NONE;
}
}
package checker;
import board.*;
import system.System;
public class NumTileChecker extends Checker {
@Override
public System.State check() {
if (Board.getInstance().getUseTile() > 3) {
return System.State.OVER_TILE_ERROR;
}
if (Board.getInstance().getRestTile() < 0) {
return System.State.NO_TILE_ERROR;
}
if (Board.getInstance().getUseTile() == 0) {
return System.State.ZERO_TILE_ERROR;
}
return System.State.NONE;
}
}
package checker;
import board.Board;
import board.TileState;
import system.System;
public class AllConnectChecker extends Checker {
@Override
public System.State check() {
for (int i=0; i<17; ++i) {
for (int j=0; j<17; ++i) {
if (Board.getInstance().getAdjacnetState().stream().filter( s -> s == TileState.Connect.ABLED).count() > 0) {
return System.State.TURN_END;
}
}
}
return System.State.GAME_END;
}
}
다음은 나머지 체커 객체들이다. 각각 타일의 연결성 검사, 타일사용 규칙 검사, 모든 철로가 연결되었는지 검사 를 하는 코드이다. 모두 공통적으로 Board 객체의 get... 메서드를 통해 데이터를 읽어와 각각의 방식으로 검사를 하여 결과 값을 리턴하는 것을 볼 수있다. 이를 통해 Board 객체를 보드 상태 관리 및 데이터 제공, Checker 객체는 정당성 검사라는 자신의 책임을 자율적으로 수행할 수 있다.
'프로젝트 이야기 > 지니어스:모노레일' 카테고리의 다른 글
5편 : 싱글턴(Singleton) 없애기 (0) | 2021.12.23 |
---|---|
4편 : GUI 구현 (0) | 2021.12.22 |
2편 : 구현하기(1) (0) | 2021.12.15 |
1편 : 설계하기 (0) | 2021.12.14 |
2년전 자바 텀프로젝트 리팩터링하기 : 들어가며 (0) | 2021.12.09 |