레벨 3가 끝나고 진짜로 이젠 미룬 피드백을 정리할 때가 왔다고 생각해서 작성하는 오목 미션 피드백이다!
1단계, 2단계
3단계, 4단계
이거에 대해 글을 작성하기 시작하면.. 너무 방대할 것 같기도 하고 인터넷에 돌아다니는 MVC 관련 블로그 들의 카피본이 될 것 같기 때문에 간단하게 리뷰에 대해서만 정리해본다.
class Controller(private val gameView: GameView) {
private val board = Board(Player(), Player())
private var lastPosition: String? = null
...
}
Controller에서 실질적인 게임이 진행되고 있어요,
Controller에는 View와의 상호작용외의 기능을 제거해보아요!
기존의 코드는 Controller에서 별일을 다했다.View를 조작하는 것 이외에도 흑-백 턴 넘기는 것을 controller에서 수행시켰다.
Controller는 Model과 View 사이를 연결해주는데, Model과 View는 서로를 알고 있으면 안된다. 근데 나는 Board를 들고 있었다... (머쓱)
그래서 OmokGame이라는 클래스를 생성하여, 이 안에서 게임이 진행되게 하고, Controller가 view와 omokGame만을 갖게 하였다.
class Controller(private val gameView: GameView, private val omokGame: OmokGame)
board.blackPlayer.put(Stone(position))
board.putStone(position)
이 코드를 보면 이 생각이 들 것이다.
같은 일을 하는 코드 아니야?
player와 board에서 각각 hands, Positions를 따로 관리하고 있어요.
매번 수를 두는 작업이 중복되어 실행이 되네요.
오목은 서로의 플레이어의 수를 게임 판위에서 서로 상호작용을 하는 구조인데, 혹시 책임이 나눠진건 아닐까 한번 고민해보아도 좋을거 같아요.
리뷰어님이 지적할 수 밖에 없는 코드...ㅎㅎ
확실히 객체지향에 대해 이해도가 거의 없었던 것 같다..(으아. ㅜㅜ)
fun putStone(turn: Turn, position: Position) {
when (turn) {
Turn.Black -> blackPlayer.put(Stone(position))
Turn.White -> whitePlayer.put(Stone(position))
}
positions.find { it == position }?.occupy()
}
바둑돌을 두는 행위를 board 내부에서만 행하도록 코드를 변경하였다.
(Board가 Player를 갖게 했는데.. 이에 대해서는 뒤에서 내용이 다시 나올지도?)
fun isPlaceable(turn: Turn, position: Position): Boolean {
return when {
turn == Turn.White -> positions.find { it == position }?.isEmpty() == true
LineJudgement(blackPlayer, position).check() -> true
ThreeJudgement(blackPlayer, whitePlayer, position).check() || FourJudgement(blackPlayer, whitePlayer, position).check() -> false
else -> positions.find { it == position }?.isEmpty() == true
}
룰이 자주 변경된다면 어떤 구조가 유연할까요?
isPlaceable에서 중요한점은 룰위반인가 아닌가의 여부이지,
3-3, 4-4에 대한 룰은 확인하는지에 대한 자세한 정보는 알필요가 없을거 같아요! 추상화해보면 어떨까요?
Board에서 3-3에 해당하는지 4-4에 해당하는지 알 필요가 있을까?
답은 ❌!! 룰 위반을 했는지 아닌지만 알 수 있으면 된다.
룰이 바뀌게 되면 4-4만 안되게 된다던지 아니면 다른 추가 룰이 생길 수 있는데, 이를 Board에서 처리하게 되면 코드 변경점이 많아질 것이다...
object Judgement {
fun line(player: Player, position: Position): Boolean {
return LineJudgement(player, position).check()
}
fun isForbiddenMove(blackPlayer: Player, whitePlayer: Player, position: Position): Boolean {
if (line(blackPlayer, position)) {
return false
}
return ThreeJudgement(blackPlayer, whitePlayer, position).check() ||
FourJudgement(blackPlayer, whitePlayer, position).check()
}
}
Judgdement라는 객체를 생성하여 이를 통해 룰을 판단하도록 했다.
line()은 5개가 완성되었는지,
isForbidden()은 금수 자리인지 확인하도록 했다.
fun isPlaceable(turn: Turn, position: Position): Boolean {
if (turn == Turn.White) {
return isEmpty(position)
}
return isEmpty(position) && !Judgement.isForbiddenMove(blackPlayer, whitePlayer, position)
}
이렇게 하면 위 처럼 복잡했던 코드가 이렇게 간결하게 판결을 내리는 코드로 탈바꿈한다. (우와아아아앙 ~)
class Player(val hand: Hand = Hand()) {
fun put(stone: Stone) {
hand.add(stone)
}
}
Player의 클래스에 대해 고민해보셔도 될거같아요, Player는 어떤 책임을 가지고 있을까요?
현실세계의 기준으로 객체화하다보니, 오목을 두는 주체로 플레이어라는 객체를 만든건 아닐까요?
플레이어라는 주체를 따로 생성하기 보단, 객체,사물의 의인화를 통해 설계하기도 해요!
불필요한 객체는 아닐지 한번 고민해보아도 좋을거 같아요!
player는 보드 위에 둔 돌을 저장하는 역할
player의 hands 도 비슷한 책임이 있는것 같아요!
객체지향에서 가장 조심해야할 점 : 현실세계를 기반으로 설계하지 마라.
이건 블랙잭 미션에서도 느꼈던 점인데 또또... 이렇게 구조를 설계하고 말았다...
player는 사실상 돌을 가지고 있는 역할인데,
기존의 구조는 Player가 내가 가진 돌 리스트를 객체화 한 Hand를 가지고 있었다. 즉.. Player 와 Hand의 책임이 같았던 것...
class Player() {
private val _stones = mutableListOf<Stone>()
val stones
get() = _stones.toList()
fun put(stone: Stone) {
_stones.add(stone)
}
}
Player가 둔 돌을 관리하도록 Hand를 제거해 책임을 옮겨주었다.
// when
blackPlayer.put(Stone(Position(HorizontalAxis.C, 15)))
blackPlayer.put(Stone(Position(HorizontalAxis.C, 14)))
blackPlayer.put(Stone(Position(HorizontalAxis.C, 12)))
blackPlayer.put(Stone(Position(HorizontalAxis.C, 11)))
blackPlayer.put(Stone(Position(HorizontalAxis.C, 10)))
// then
assertThat(board.isPlaceable(Turn.Black, position)).isTrue
이 테스트에서는 특정 상태에서의 isPlaceable를 테스트하는 로직입니다.
하지만, 준비단계에서 put을 여러번 실행이 되고있는데요,
put 함수에 문제가 생긴다면, 이 테스트가 실패할거 같아요.
그렇다면, isPlaceable 테스트에 대해 put 함수에 의존한다고 볼수도 있을거 같아요!
초기값으로 blackPlayer을 만들어주면 어떨까요?
위에서 Player를 아무 생성자 없이 stones를 필드로 관리했었다.
하지만 알다시피.. 무엇이 문제겠는가? 테스트하는데 어려움이 생긴다.
class Player(stones: List<Stone> = listOf()) {
private var _stones = stones
val stones
get() = _stones.toList()
fun put(stone: Stone) {
_stones = _stones + stone
}
}
위와 같이 Player가 stones를 초기에 생성자로 들고 있게 했다.
내용을 떠나서, 이 말이 정말 중요함을 명시해야한다.
실험할 때 변인들을 관리하는 것 처럼 테스트를 할 때도 동일하다.
테스트 코드 내에서는 내가 테스트하고 싶은 것을 다른 로직과 무관하게 온전히 테스트할 수 있어야 한다.
fun isPlaceable(turn: Turn, position: Position): Boolean {
if (turn == Turn.White) {
return isEmpty(position)
}
return isEmpty(position) && !Judgement.isForbiddenMove(blackPlayer, whitePlayer, position)
}
룰이 변경될수 있다면, 어떻게 유연하게 대처할수 있을까요?
룰을 주입받아 사용해보면 어떨까요?
객체지향에서 중요한 여러가지 중 하나가 변경에 유연하게 대처하는 것이다.
이때 Interface를 만들어, 이를 상속받게 만들면 변경점이 생겨도 쉽게 교체할 수 있게 된다.
또한, 이를 쉽게 교체하기 위해서는 외부주입을 해주면 된다 !
class Board(val judgement: Judgement, val blackPlayer: Player, val whitePlayer: Player)
class RenjuJudgement() : Judgement
interface Judgement {
fun line(player: Player, position: Position): Boolean
fun isForbiddenMove(blackPlayer: Player, whitePlayer: Player, position: Position): Boolean
}
Board를 다음과 같이 Judgement를 입력받게 한다.
현재 사용하는 룰은 RenjuRule로 명명하고 Judement를 상속받게 한다.
이를 통해 룰이 변경되더라도 쉽게 대처 가능해진다!
디미터의 법칙은 최소한의 지식 원칙이라고도 불린다.
즉, 객체가 다른 객체에 대해 지나치게 많은 정보에 대해 알고 있으면 안됨을 의미한다.
이 법칙을 준수하게 되면 캡슐화가 높아지고 객체의 자율성과 응집도를 높일 수 있다.
짤막 회고
이 미션에서 local DB에 대해서도 학습했었는데, 리뷰가 이 부분보다는 객체지향적인 부분이 많았다. 오랜만에 리뷰를 보니, 나도 참.. 객체지향에 대해 아무것도 몰랐구나 싶었다. 그리고 리뷰어인 잭슨은 정말 친절하다고 느꼈다....
고쳤습니다(헤헤) -> 잘하셨어요 ~ 근데 ~ 의 무한루프...
참을성이 진짜 좋으신듯.. 당시에도 잭슨의 리뷰가 가장 좋았다고 느꼈는데 오랜만에 봐도 리뷰 너무 잘해주셔서 고맙다.. (흑흑..)
남은 피드백도 빨리 정리하길 바란다 ! (제발)