-
Notifications
You must be signed in to change notification settings - Fork 411
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
5단계 - 자동차 경주(리팩터링) #918
base: relkimm
Are you sure you want to change the base?
5단계 - 자동차 경주(리팩터링) #918
Conversation
constructor(car: Car) : this( | ||
id = car.id, | ||
name = car.name, | ||
position = car.position, | ||
) | ||
|
||
fun move(movePolicy: MovePolicy): Car { | ||
if (movePolicy.canMove()) { | ||
this.position = this.position.forward() | ||
} | ||
return Car(car = this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
immutable
하게 만들어 보았습니다~🙆♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
새로운 car 객체를 잘 생성해주신 거 같은데요~
position 프로퍼티를 불변(val)으로 가지고 있어야 진정한 불변 객체가 아닐까 싶습니다! 🤔
현재는 car 객체의 position 값이 var 로 선언되어있네요~
class CarGroup(private var cars: List<Car>) { | ||
fun move() { | ||
this.cars.forEach { car -> | ||
this.cars = this.cars.map { car -> | ||
val oil = OilStation.generateOilRandomly() | ||
car.move(movePolicy = OilPolicy(oil = oil)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CarGroup
이 move
할 때도 immutable 하게 cars 를 반환할까 싶었는데!
아래 getPositions
api 를 사용하는 것도 괜찮은 것 같아서 그대로 두었습니다.😭😭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CarGroup 이 move 할 때도 immutable 하게 cars 를 반환하는 것이 좀 더 안전한 프로그램 아닐까 싶은데요~ 🤔
getPositions() 를 호출하기 전까지 CarGroup 이 변경 불가능하다는 것을 보장받는 것이 전 좋아보입니다!
value class CarName( | ||
val value: String, | ||
) { | ||
init { | ||
require(this.value.isNullOrBlank().not()) { "자동차 이름은 비어 있을 수 없습니다." } | ||
require(this.value.length <= MAX_NAME_LENGTH) { "자동차 이름은 5 글자를 초과할 수 없습니다." } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
자동차 이름이 비어있을 경우의 유효성 검사도 추가해 보았어요~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
꼼꼼하게 피드백 반영해주셨네요~ 👍
package racingcar | ||
|
||
data class RoundResult( | ||
val carPositions: List<CarPosition> | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RoundResult
가 도메인 모델이라기 보다는 화면에 표시하기 위한 dto 느낌이 들어서
RoundResult
대신 CarGroup
의 getCarPositions
를 활용하는 방향으로 리팩토링해 보았습니다.🙇♂️
package racingcar.view.container | ||
|
||
import racingcar.view.component.Component | ||
|
||
interface Container : Component |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요 친구는 프론트 개발 패턴 중에서 container / presentational 컴포넌트 패턴
을 표현하기 위해 만들어 보았네요!
container 컴포넌트
는 (Container) 로직을 수행하거나 store 에서 데이터를 조회하고
presentational 컴포넌트
에게 데이터를 전달하는 역할을 수행하고 있고
presentational 컴포넌트
(Component) 는 단순히 생성자 통해서 전달 받은 데이터를
화면에 표시하는 역할을 수행하도록 만들어 보았네요~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
view 영역에 대한 설계를 많이 고민하시고 적용해주셨네요~
프론트 개발 영역은 제가 잘 모르는 부분이라 해당 패턴에 대해서는 코멘트를 드리기 쉽지 않네요.. 😭
자동차 미션의 목적에 좀 더 초점을 맞춰서 리뷰 드리도록 하겠습니다~ 🙏
이때 domain 영역이 view 에 의존하지 않도록 잘 분리해주신 거 같습니다
아쉬웠던 건 각종 InputComponent
가 생성되자마자 onCommand()
를 바로 실행해 입력을 받고 있는데
입력을 받도록 명령하는 것은 controller 영역에서 따로 통제하고 controller 를 통해 입력값을 모델에 전달하는 것이 역할 분리가 좀 더 낫지 않을까요?
마찬가지로 ResultComponent
는 controller 에 의해 전달받은 데이터를 그려주는 역할만 하는 것이 적절해보이는데
render() 메소드 안에서 store 에 있는 모델 데이터를 받아오는 역할까지 수행해 여러 책임을 갖는 것으로 보이네요 🤔
현재 구조에서는 controller 의 영역이 따로 없이 view 에서 모델 데이터를 통제하는 역할까지 수행하는 것 같네요~
MVC 패턴을 고민해보면서 관심사의 분리를 시도해보는 것은 어떨까요? 🙇♂️
class RoundContainer(private val round: Round) : Container { | ||
init { this.roundStart() } | ||
private fun roundStart() { | ||
val carGroup = CarGroupStore.getState() | ||
this.round.start(carGroup = carGroup) | ||
CarGroupStore.setState(state = carGroup) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
컴포넌트가 마운트 되었을 때의 동작을 어디서 수행하면 좋을까? 고민을 하다가 코틀린의 init
함수를 활용해 보았습니다~
abstract class Store<T> { | ||
protected val listeners = HashSet<Listener>() | ||
|
||
abstract fun getState(): T | ||
abstract fun setState(state: T) | ||
abstract fun subscribe(listener: Listener): UnSubscribe | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
listeners
와 subscribe
함수를 추가해 보았는데! UI 가 독립적으로
그리고 비동기적으로
랜더링된다면 의미가 있을 것 같은데 지금은 동기적으로
실행되고 있다 보니, 활용을 못 했네요😭😭
abstract class Store<T> { | ||
protected val listeners = HashSet<Listener>() | ||
open class Store<T>(initialState: T) { | ||
private var state = initialState | ||
private val listeners = HashSet<Listener>() | ||
|
||
abstract fun getState(): T | ||
abstract fun setState(state: T) | ||
abstract fun subscribe(listener: Listener): UnSubscribe | ||
fun getState(): T { | ||
return this.state | ||
} | ||
fun setState(state: T) { | ||
this.state = state | ||
this.listeners.forEach { listener -> listener() } | ||
} | ||
fun subscribe(listener: Listener): UnSubscribe { | ||
listeners.add(listener) | ||
return { listeners.remove(listener) } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
전역 상태 관리할 수 있는 store 를 공통으로 사용하기 위해 요렇게 개선해 보았습니다~🙆♂️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5단계 미션 수행하시느라 고생 많으셨습니다! 👍
몇가지 코멘트 남겼으니 확인 부탁드려요~
자동차 미션 얼마 남지 않았네요!
마지막까지 화이팅입니다~ 🔥 🔥
constructor(car: Car) : this( | ||
id = car.id, | ||
name = car.name, | ||
position = car.position, | ||
) | ||
|
||
fun move(movePolicy: MovePolicy): Car { | ||
if (movePolicy.canMove()) { | ||
this.position = this.position.forward() | ||
} | ||
return Car(car = this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
새로운 car 객체를 잘 생성해주신 거 같은데요~
position 프로퍼티를 불변(val)으로 가지고 있어야 진정한 불변 객체가 아닐까 싶습니다! 🤔
현재는 car 객체의 position 값이 var 로 선언되어있네요~
class CarGroup(private var cars: List<Car>) { | ||
fun move() { | ||
this.cars.forEach { car -> | ||
this.cars = this.cars.map { car -> | ||
val oil = OilStation.generateOilRandomly() | ||
car.move(movePolicy = OilPolicy(oil = oil)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CarGroup 이 move 할 때도 immutable 하게 cars 를 반환하는 것이 좀 더 안전한 프로그램 아닐까 싶은데요~ 🤔
getPositions() 를 호출하기 전까지 CarGroup 이 변경 불가능하다는 것을 보장받는 것이 전 좋아보입니다!
val value: String, | ||
) { | ||
init { | ||
require(this.value.isNullOrBlank().not()) { "자동차 이름은 비어 있을 수 없습니다." } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마이너하지만 .not()
을 사용하게 되면서 가독성이 약간은 떨어지게 된 거 같아요~
아래와 같이 좀 더 간결하게 작성하는 것도 좋을 거 같습니다!
require(this.value.isNullOrBlank().not()) { "자동차 이름은 비어 있을 수 없습니다." } | |
require(this.value.isNotBlank()) { "자동차 이름은 비어 있을 수 없습니다." } |
value class CarName( | ||
val value: String, | ||
) { | ||
init { | ||
require(this.value.isNullOrBlank().not()) { "자동차 이름은 비어 있을 수 없습니다." } | ||
require(this.value.length <= MAX_NAME_LENGTH) { "자동차 이름은 5 글자를 초과할 수 없습니다." } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
꼼꼼하게 피드백 반영해주셨네요~ 👍
package racingcar.view.container | ||
|
||
import racingcar.view.component.Component | ||
|
||
interface Container : Component |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
view 영역에 대한 설계를 많이 고민하시고 적용해주셨네요~
프론트 개발 영역은 제가 잘 모르는 부분이라 해당 패턴에 대해서는 코멘트를 드리기 쉽지 않네요.. 😭
자동차 미션의 목적에 좀 더 초점을 맞춰서 리뷰 드리도록 하겠습니다~ 🙏
이때 domain 영역이 view 에 의존하지 않도록 잘 분리해주신 거 같습니다
아쉬웠던 건 각종 InputComponent
가 생성되자마자 onCommand()
를 바로 실행해 입력을 받고 있는데
입력을 받도록 명령하는 것은 controller 영역에서 따로 통제하고 controller 를 통해 입력값을 모델에 전달하는 것이 역할 분리가 좀 더 낫지 않을까요?
마찬가지로 ResultComponent
는 controller 에 의해 전달받은 데이터를 그려주는 역할만 하는 것이 적절해보이는데
render() 메소드 안에서 store 에 있는 모델 데이터를 받아오는 역할까지 수행해 여러 책임을 갖는 것으로 보이네요 🤔
현재 구조에서는 controller 의 영역이 따로 없이 view 에서 모델 데이터를 통제하는 역할까지 수행하는 것 같네요~
MVC 패턴을 고민해보면서 관심사의 분리를 시도해보는 것은 어떨까요? 🙇♂️
// when | ||
sut.move(movePolicy = OilPolicy(oil = 충분한_연료)) | ||
// | ||
sut.position shouldBe Position(value = 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
car 는 position 값이 변할 수 있는 객체네요~
불변 객체를 만드는 것을 다시 시도해보면 어떨까요?
안녕하세요 인근님~!
5단계 우선 완료되어서 리뷰 요청 드립니다.🙇♂️