Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step1 지뢰 찾기(그리기) #377

Merged
merged 6 commits into from
Nov 29, 2023
Merged

Conversation

jaylene-shin
Copy link

@jaylene-shin jaylene-shin commented Nov 28, 2023

윤혁님 안녕하세요, 처음 뵙겠습니다!!
step1 미션 리뷰 요청드립니다.

리뷰 중점 사항

  1. 지뢰가 위치할 (랜덤) 좌표와 빈 좌표 정보를 각각 생성하여 지뢰판을 구성하였습니다. 현재 방식이 지뢰판 생성 중 발생 가능한 반복 탐색(e.g. 지뢰판의 모든 좌표가 랜덤하게 생성된 지뢰 좌표와 중복되는지 탐색. O(height*width))을 최소화하는 방식이라고 판단하였는데요...! 옳은 판단인지 피드백을 받고 싶습니다!

  2. 지뢰판의 map은 객체 내에서는 가변이지만 외부에서는 불변을 보장할 수 있도록 방어적 복사를 활용하였습니다. 추후 스레드 안정성을 고려했을 때 현재 방식은 최선의 방식일지 확인 부탁드립니다!

Copy link
Member

@malibinYun malibinYun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 :) 1단계 미션 고생 많으셨어요!
생각해볼만한 곳에 코멘트 남겨두었어요.
다음 미션 진행을 위해 머지할게요!
피드백은 다음 미션에 반영해주세요!

Comment on lines +3 to +5
data class Cell(
val state: CellState
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cell class가 별 다른 역할 없이 CellState 를 가지고만 있는 걸로 보여요.
CellState자체가 Cell로 표현되어도 좋겠어요!

혹은 이후 한 칸에 대한 객체를 더 표현하기 위함이었다면, sealed class를 활용해보셔도 좋겠네요 :)

Copy link
Author

@jaylene-shin jaylene-shin Dec 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

칸 객체를 표현할 수 있도록 sealed interface인 Cell 을 생성하고
Mine과 Empty를 하위 클래스로 구현하였습니다!

Comment on lines +3 to +7
class MineMap(
private val _values: MutableMap<Position, Cell> = mutableMapOf()
) {
val values: Map<Position, Cell>
get() = _values
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지뢰판의 map은 객체 내에서는 가변이지만 외부에서는 불변을 보장할 수 있도록 방어적 복사를 활용하였습니다.

방어적 복사를 활용했다고 보기에는 어려워보여요.
생성자에서 MutableCollection을 받을 때, 다른 개발자가 외부의 참조를 실수로 다뤘을 때, MineMap 객체 내부가 변경되는 상황이 발생할 수 있어요.
아래 코드를 보시면 쉽게 이해하실 수 있을거예요 :)

val mutableMap = mutableMapOf()
val mineMap = MineMap(mutableMapOf())
mutableMap.clear()

생성자에서 전달 받은 컬렉션을 복사해서 멤버변수에 할당하는 것은 어떨까요?

Copy link
Author

@jaylene-shin jaylene-shin Dec 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 네네 설명해주셔서 감사합니다..! 방어적 복사는 외부 참조를 끊어내는게 핵심인데 위 코드에서는 끊어내질 못하고 있네요..!
(step2 최종 코드에는 제거되어 있지만) 방어적 복사를 올바르게 활용하도록 수정하였습니다!

class MineMap(
    values: Map<Position, Cell> = mapOf()
) {
    private val _values = values.toMutableMap()
    val values: Map<Position, Cell>
        get() = _values.toMap()
     
    // 이하 생략
}

Comment on lines +5 to +7
val mineMap = MineMap()
minePositions.forEach { mineMap.plantCell(it, Cell(CellState.MINE)) }
emptyPositions.forEach { mineMap.plantCell(it, Cell(CellState.EMPTY)) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plantCell을 호출함으로써 지뢰판을 초기화 하기보다,
지뢰판을 생성할 때 생성자에 필요한 모든 정보를 넘겨 만들어내는 것은 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다!
plantCell을 활용하다보니 MineMap의 프로퍼티를 mutable로 써야했었는데요. 이 문제를 같이 해결할 수 있을 것 같습니다..!
피드백 주신 방향으로 수정 완료했습니다!

Comment on lines +9 to +12
require(height > 0) { "높이는 0 이거나 음수일 수 없습니다" }
require(width > 0) { "너비는 0 이거나 음수일 수 없습니다" }
require(mineCount > 0) { "지뢰 개수는 0 이거나 음수일 수 없습니다" }
require(height * width >= mineCount) { "지뢰 개수는 (높이 x 너비) 개수를 초과할 수 없습니다" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 값이 들어와서 예외가 발생했는지 입력값도 메시지에 같이 표시해주면 디버깅할 때 더욱 도움이 될 것으로 기대돼요 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

트러블슈팅에 훨씬 유리하겠네요..! 수정하였습니다!

Comment on lines +7 to +17
tailrec fun generateMinePositions(
minePositions: Positions = Positions()
): Positions {
if (minePositions.size == mineMapMeta.mineCount) { return minePositions }
val randomMinePosition = positionSelector.select(mineMapMeta)
return if (randomMinePosition in minePositions) {
generateMinePositions(minePositions)
} else {
generateMinePositions(minePositions + randomMinePosition)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지뢰가 위치할 (랜덤) 좌표와 빈 좌표 정보를 각각 생성하여 지뢰판을 구성하였습니다. 현재 방식이 지뢰판 생성 중 발생 가능한 반복 탐색(e.g. 지뢰판의 모든 좌표가 랜덤하게 생성된 지뢰 좌표와 중복되는지 탐색. O(height*width))을 최소화하는 방식이라고 판단하였는데요...! 옳은 판단인지 피드백을 받고 싶습니다!

운이 좋지 않으면 굉장히 많이 실행되어서 시간이 꽤 걸릴지도 모르겠어요.

아래 형태로 접근 방식을 바꿔보는 건 어떨까요?
로직적인 것만 참고해주시고, 이름은 지영님이 잘 지어보시면 좋겠어요 :)

val allPosition = ~~
val minePosition = RandomSelector.select(allPosition, 개수) // 전달 받은 positions을 shuffle하고, 개수만큼 take() 한positions을 반환
val cells = minePosition.map { Mine() } + (allPosition - minPosition).map { Block() }
~~

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재귀 써야한다는 생각에 빠져있었는데, shuffle-take 연산이 훨씬 효율적이겠네요..!! 감사합니다!
수정하였습니다!

Comment on lines +3 to +5
class Positions(
private val positions: Set<Position> = emptySet()
) : Set<Position> by positions {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일급 컬렉션을 kotlin delegate를 활용해서 작성해주셨네요!
delegate를 활용하면 외부에서 활용하지 않아도 될 컬렉션의 api 조차도 모두 공개되기 때문에 일급 컬렉션을 만드는 의미가 희석된다고 생각해요.
필요한 기능들만 외부에 공개해보는 건 어떨까요?

사용하는 것은 편리해질 수 있겠지만, Collection을 래핑만 한 것과 크게 다르지 않다는 생각이에요 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존에는 내부 프로퍼티를 바로 순회할 수 있게 delegate을 사용했었는데요.
불필요한 컬렉션 api 를 모두 공개한다면, 일급 컬렉션의모든 행위를 내부에서 관리한다는 목적이 희석될 수 있겠네요...!
좋은 피드백 감사합니다. 👍
delegate 제거하도록 수정하였습니다!

Comment on lines +28 to +29
// then
assertEquals(mineMapMeta.getCellCount(), mineMap.values.size)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 검증문은 assertJ 혹은 kotest를 활용해보는 건 어떨까요?

Copy link
Author

@jaylene-shin jaylene-shin Dec 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가독성 확보와 메서드 체이닝 활용 측면에서 더 유리할 것 같습니다!
step2에서는 assertJ의 assertThat().isEqualTo()... 방식으로 수정하였습니다! (예시)

Comment on lines +30 to +33
// then
assertEquals(2, mineMap.size)
assertEquals(CellState.EMPTY, mineMap.values[Position(1, 1)]?.state)
assertEquals(CellState.EMPTY, mineMap.values[Position(2, 2)]?.state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert 구문을 나열하는 것과 assertAll로 감싸는 것은 어떤 차이점이 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert 구문 나열 시 실패한 구문 이후로 테스트가 수행되지 않을 것 같습니다..!
assertSoftly를 도입해 위 문제를 해결하였습니다!

@malibinYun malibinYun merged commit e58cc6c into next-step:jaylene-shin Nov 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants