== 피라미드 오르기 == 2h 20m 28s 아이디어는 JuNe 선배가 말했던 것이다.(저번 자바 컨퍼런스에서 였던가..) 한 번 나선형으로 진행되는 것을 같은 층으로 본다. 그러면 가장 바깥쪽은 1층, 다음 안쪽은 2층 이런 식으로 안쪽으로 갈수록 높이가 높아진다. 한 사람이 피라미드를 한 바퀴 돌고 다음 층으로 올라가면서 자신이 들렀던 곳이 몇 번째인지, 좌표는 무엇인지 기억한다. 한 층을 다 돌면 시작했던 자리로 돌아오기 때문에 중복해서 기억한 좌표는 지우고 다음 층으로 이동한다. 하지만 여지껏 그러한 접근법을 알고서도 TDD로 풀지를 못했었다. 매번 나선형 "행렬"에 어떻게 숫자를 새길지만 생각했기 때문이다. 그러다 보니 2차원 배열의 인덱스를 조작하는 수준에서 생각이 벗어나질 못했다. 하지만 사실은 움직임(이전의 인덱스 조작), 움직인 점들, 행렬을 따로 생각할 수 있었다. 아! 이렇게 테스트 하면 되겠구나! TDD로 풀었다는 점이 기쁘다. 처음부터 너무 메서드를 어디에 속하게 할 지 고민하지 않고 시작한 것이 유용했다. 그 결과로 예전 같으면 생각하지 못했을 Direction클래스와 그 하위 클래스가 탄생했다. 또한 행렬은 최종 결과물을 저장하고 보여주는 일종의 뷰처럼 쓰였다. 현재는 행렬 구성이 비효율적이다. 움직였던 기록을 가지고 행렬을 구성하기를 반복한다.이것을 수정할 때 좀더 효율적으로 작동하게 만들어야겠다. Mover클래스, Array클래스의 종료검사, 테스트 케이스는 확실히 Refactoring이 필요하다. {{{~cpp import unittest ROW = 0 COL = 1 class Direction: def move(self, coordinate, board): if ( board.isWall( self.next(coordinate) ) ): return coordinate return self.next(coordinate) class Down(Direction): def next(self, coordinate): return (coordinate[ROW] + 1, coordinate[COL]) class Up( Direction ): def next(self, coordinate): return (coordinate[ROW] - 1, coordinate[COL]) class Right(Direction): def next(self, coordinate): return (coordinate[ROW], coordinate[COL] + 1 ) class Left(Direction): def next(self, coordinate): return (coordinate[ROW], coordinate[COL] - 1 ) class Board: def __init__(self): self.size = 0 def setBoundary( self, start, end ): self.start = start self.end = end def isWall( self, coordinate ): if ( coordinate[ROW] < self.start ): return True elif ( coordinate[ROW] >= self.end ): return True elif ( coordinate[COL] < self.start ): return True elif ( coordinate[COL] >= self.end ): return True return False def decreaseBoundary(self, amount): self.setBoundary( self.start+amount, self.end-amount ) class Mover: def __init__(self): self.coordinate = (0,0) self.moveCount = 0 self.history = [] def position(self): return self.coordinate def goStraight(self, direction, board): while ( not self.coordinate == direction.move(self.coordinate, board) ): self.history.append( Point( self.coordinate, self.moveCount+1 ) ) self.moveCount += 1 self.coordinate = direction.move( self.coordinate, board ) self.history.append( Point( self.coordinate, self.moveCount+1 ) ) def mark(self, point, value): point.value = value def _setMoveCount(self, count): self.moveCount = count def _setPosition(self, coordinate): self.coordinate = coordinate def getHistory(self): return self.history def turnRound(self, board): self.goStraight( Right(), board ) self.goStraight( Down(), board ) self.goStraight( Left(), board ) self.goStraight( Up(), board ) board.decreaseBoundary(1) self.eraseLastMovement() self._setPosition( self.nextFloor() ) def nextFloor(self): return (self.coordinate[ROW]+1, self.coordinate[COL]+1 ) def eraseLastMovement(self): self._setMoveCount( self.history.pop().value - 1 ) class Point: def __init__(self, coordinate, value): self.coordinate = coordinate self.value = value class Array: def __init__(self, size): self.matrix = [] for i in range(size): self.matrix.append( [Point((-1,-1),-1)] * size ) def isAllFilled(self): for row in self.matrix: for point in row: if ( point.value < 0 ): return False return True def _fillAll(self): for row in self.matrix: for point in row: point.value = 1 def get(self, row, col): return self.matrix[row][col] def construct(self, pointList): for p in pointList: self.matrix[p.coordinate[ROW]][p.coordinate[COL]] = p def printValues(self): for row in self.matrix: for point in row: print point.value, print "\t", print class SpiralArrayTest(unittest.TestCase): def setUp(self): self.board = Board() self.size = 10 self.board.setBoundary(start=0, end=self.size) self.mover = Mover() self.array = Array( self.size ) def testMoveDown(self): self.assertEquals( (1,0), Down().move( (0,0), self.board ) ) self.assertEquals( (self.size-1,0), Down().move( (self.size-1,0), self.board ) ) def testMoveUp(self): self.assertEquals( (0,0), Up().move( (0,0), self.board ) ) self.assertEquals( (self.size-1,0), Up().move( (self.size,0), self.board ) ) def testIsWall(self): self.assertEquals( False, self.board.isWall( (0,0) ) ) self.assertEquals( True, self.board.isWall( (-1,0) ) ) self.assertEquals( True, self.board.isWall( (0,-1) ) ) self.assertEquals( True, self.board.isWall( (0,self.size) ) ) self.assertEquals( True, self.board.isWall( (self.size,0) ) ) def testGoStraightRight(self): self.mover.goStraight(Right(), self.board) self.assertEquals( (0,self.size-1), self.mover.position() ) def testMark(self): point = Point((0,0),0) self.mover._setMoveCount(1) self.mover.mark( point, self.mover.moveCount ) self.assertEquals( 1, point.value ) def testStorePointCoordinate(self): self.mover.goStraight( Down(), self.board ) points = self.mover.getHistory() self.assertEquals( (self.size-1,0), points.pop().coordinate ) self.assertEquals( (self.size-2,0), points.pop().coordinate ) def testStoreMoveCount(self): self.mover._setPosition((self.size-1, self.size-1)) self.mover.goStraight( Left(), self.board ) points = self.mover.getHistory() self.assertEquals( self.size, points.pop().value ) self.assertEquals( self.size-1, points.pop().value ) def testTurnRound(self): self.mover._setPosition( (0,0) ) self.mover.turnRound(self.board) self.assertEquals( (1,0), self.mover.getHistory().pop().coordinate ) def testIsFinished(self): self.assertEquals( False, self.array.isAllFilled() ) self.array._fillAll() self.assertEquals( True, self.array.isAllFilled() ) def testConstructArray(self): self.mover._setPosition( (0,0) ) self.mover.turnRound(self.board) self.array.construct( self.mover.getHistory() ) self.assertEquals( (0,0), self.array.get(0,0).coordinate ) self.assertEquals( (self.size-1, self.size-1), self.array.get(self.size-1, self.size-1).coordinate ) if __name__ == "__main__": ## unittest.main() inputSize = input() board = Board() board.setBoundary(0, inputSize ) array = Array(inputSize) mover = Mover() mover._setPosition((0,0)) while not ( array.isAllFilled() ): mover.turnRound(board) array.construct(mover.getHistory()) array.printValues() }}} == 피라미드 오르기 Refacotring == 1h 58m 26s 지난 번 리팩토링 대상이었던 Mover클래스, Array클래스의 종료검사, 테스트 케이스를 리팩토링 했다. 테스트 케이스와 Array클래스는 쉽게 리팩토링 할 수 있었다. 하지만 Mover클래스를 손대는데 오래 걸렸다. goStraight 전 버전은 Direction클래스를 이용해서 이동한(벽을 만나면 이동하지 않음) 위치를 얻어내고, 이동한 기록을 저장하는 형식이었다. 벽에 대한 검사가 겹치는 것 같아 mover에서 바로 벽을 검사하고 벽에 들어서면 종료하는 것으로 만들었다. 그러고 보니 따로 카운트 할 필요가 없어 moveCount변수를 없앴다. mover가 종료 조건도 검사하는데 board 넓이만큼 이동했으면 끝나는 것이기 때문이다. 그런데 벽에 들어서야 종료하다 보니까 mover를 벽에 들어가기 전에 위치로 되돌려놓아야 했다. 그래서 direction에 모두 previous 메서드가 생겼다. 한데 다음 번 goStraight를 할 때는 이미 이동했던 기록이 남아있게 되었다. 그래서 매번 goStraight를 할 때마다 마지막 이동 기록을 삭제했다. 그러다보니 board크기가 1일 경우는 이동한 기록이 모두 지워져버리는 것이 아닌가. 조잡하지만 예외 처리를 해주었다. 경계조건이 참 미묘하다는 것을 느꼈다. 시작과 끝을 어떻게 할 것인가? 한참을 헤매다보니 더 나은 방법이 있는데도 찾질 못하는 것 같다. 코드 안에서 헤매기 보다는 정확히 생각을 정리해서 구현해야 한다. 이것 해보고 저것 해보는 사이에 시간이 너무 많이 흘러갔다. 결국에 답은 나왔지만, 이보다 빨리 할 수 있을 것이다. {{{~cpp import unittest ROW = 0 COL = 1 class Direction: pass class Down(Direction): def next(self, coordinate): return (coordinate[ROW] + 1, coordinate[COL]) def previous(self, coordinate): return Up().next( coordinate ) class Up( Direction ): def next(self, coordinate): return (coordinate[ROW] - 1, coordinate[COL]) def previous(self, coordinate): return Down().next( coordinate ) class Right(Direction): def next(self, coordinate): return (coordinate[ROW], coordinate[COL] + 1 ) def previous(self, coordinate): return Left().next( coordinate ) class Left(Direction): def next(self, coordinate): return (coordinate[ROW], coordinate[COL] - 1 ) def previous(self, coordinate): return Right().next( coordinate ) class Board: def __init__(self, initialSize): self.size = initialSize self.setBoundary( 0, self.size ) def setBoundary( self, start, end ): self.start = start self.end = end def isWall( self, coordinate ): if ( coordinate[ROW] < self.start ): return True elif ( coordinate[ROW] >= self.end ): return True elif ( coordinate[COL] < self.start ): return True elif ( coordinate[COL] >= self.end ): return True return False def decreaseBoundary(self, amount): self.setBoundary( self.start+amount, self.end-amount ) class Mover: def __init__(self): self.coordinate = (0,0) self.history = [] def position(self): return self.coordinate def goStraight(self, direction, board): while ( not board.isWall( self.coordinate ) ): self.history.append( Point( self.coordinate, self.moveCount() ) ) self.coordinate = direction.next( self.coordinate ) self.coordinate = direction.previous( self.coordinate ) def mark(self, point, value): point.value = value def moveCount(self): return len( self.history ) + 1 def setPosition(self, coordinate): self.coordinate = coordinate def getHistory(self): return self.history def turnRound(self, board): for direction in [Right(), Down(), Left()]: self.goStraight( direction, board ) self.eraseLastMovement() self.goStraight( Up(), board ) if ( board.end - board.start != 1 ): self.eraseLastMovement() self.setPosition( self.nextFloor() ) def nextFloor(self): return (self.coordinate[ROW]+1, self.coordinate[COL]+1 ) def eraseLastMovement(self): self.history.pop() def isFinished( self, board ): return board.size * board.size < self.moveCount() def lastHistory( self ): return self.history[-1] class Point: def __init__(self, coordinate, value): self.coordinate = coordinate self.value = value class Array: def __init__(self, size): self.matrix = [] for i in range(size): self.matrix.append( [Point((-1,-1),-1)] * size ) def get(self, row, col): return self.matrix[row][col] def construct(self, pointList): for p in pointList: self.matrix[p.coordinate[ROW]][p.coordinate[COL]] = p def printValues(self): for row in self.matrix: for point in row: print point.value, print "\t", print class SpiralArrayTest(unittest.TestCase): def setUp(self): self.size = 10 self.board = Board(self.size) self.board.setBoundary(start=0, end=self.size) self.mover = Mover() self.array = Array( self.size ) class DirectionTest(SpiralArrayTest): def testNext(self): self.assertEquals( (1,0), Down().next( (0,0) ) ) def testPrevious(self): self.assertEquals( (0,0), Down().previous( (1,0) ) ) class BoardTest(SpiralArrayTest): def testIsWall(self): self.assertEquals( False, self.board.isWall( (0,0) ) ) self.assertEquals( True, self.board.isWall( (-1,0) ) ) self.assertEquals( True, self.board.isWall( (0,-1) ) ) self.assertEquals( True, self.board.isWall( (0,self.size) ) ) self.assertEquals( True, self.board.isWall( (self.size,0) ) ) class MoverTest(SpiralArrayTest): def testGoStraightRight(self): self.mover.goStraight(Right(), self.board) self.assertEquals( (0,self.size-1), self.mover.position() ) def testStorePointCoordinate(self): self.mover.goStraight( Down(), self.board ) points = self.mover.getHistory() self.assertEquals( (self.size-1,0), points.pop().coordinate ) self.assertEquals( (self.size-2,0), points.pop().coordinate ) def testStoreMoveCount(self): self.mover.setPosition((self.size-1, self.size-1)) self.mover.goStraight( Left(), self.board ) points = self.mover.getHistory() self.assertEquals( self.size, points.pop().value ) self.assertEquals( self.size-1, points.pop().value ) def testStoreMovementWhenTurnRoundAndBoudaryNotOne(self): self.mover.setPosition( (0,0) ) self.mover.turnRound(self.board) points = self.mover.getHistory() self.assertEquals( (self.size-1) * 4 , len( points ) ) def testStoreMovementWhenTurnRoundAndBoudaryOne(self): self.board = Board( initialSize = 1 ) self.mover.setPosition( (0,0) ) self.mover.turnRound(self.board) points = self.mover.getHistory() self.assertEquals( 1 , len( points ) ) def testTurnRoundWithBoundary0(self): self.mover.setPosition( (0,0) ) self.mover.turnRound(self.board) self.assertEquals( (1,0), self.mover.lastHistory().coordinate ) def testIsFinished(self): self.board = Board( initialSize = 1 ) self.assertEquals( False, self.mover.isFinished( self.board ) ) self.mover.turnRound( self.board ) self.assertEquals( True, self.mover.isFinished( self.board ) ) def testRun(self): self.board = Board( initialSize = 3 ) self.mover.setPosition( (0,0) ) self.mover.turnRound(self.board) self.assertEquals( (1,0), self.mover.lastHistory().coordinate ) self.assertEquals( (1,1), self.mover.position() ) self.board.decreaseBoundary(1) self.mover.turnRound(self.board) self.assertEquals( (1,1), self.mover.lastHistory().coordinate ) class ArrayTest(SpiralArrayTest): def testConstructArray(self): self.mover.setPosition( (0,0) ) self.mover.turnRound(self.board) self.array.construct( self.mover.getHistory() ) self.assertEquals( (0,0), self.array.get(0,0).coordinate ) self.assertEquals( (self.size-1, self.size-1), self.array.get(self.size-1, self.size-1).coordinate ) if __name__ == "__main__": unittest.main() inputSize = input() board = Board(inputSize) mover = Mover() mover.setPosition((0,0)) while ( not mover.isFinished( board ) ): mover.turnRound(board) board.decreaseBoundary(1) array = Array(inputSize) array.construct(mover.getHistory()) array.printValues() }}}