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

[다리 건너기] 4주차 미션 리펙토링 #1

Open
wants to merge 40 commits into
base: main
Choose a base branch
from

Conversation

apptie
Copy link
Owner

@apptie apptie commented Dec 7, 2022

미션 - 다리 건너기

리펙토링 목록

  • PlayerStateBridge간 순환 참조 제거
  • GameCommand에서 사용하고 있던 잘못된 이름 수정
  • 해당 클래스에서 사용하는 상수 및 메세지를 모두 내부에서 관리
  • 불필요한 GameStatus 일부 상태 삭제
  • GuideView에서 출력하는 메세지를 OutputView로 통합

Copy link

@gitchannn gitchannn left a comment

Choose a reason for hiding this comment

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

지금 시간이 없어서 내일 다시 와서 보겠습니다!!!
몇주전에는 처음 코드 보고 이해 안되는 내용이 많았는데,
보다보니 점점 감탄하게됩니다...ㅎㅎ

gameStatus = controller.process(gameStatus);
}
}
}

Choose a reason for hiding this comment

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

지민님 혹시 Runnercontroller는 어떤 차이가 있나요??
지민님이 구현하고자 하는 모델은 어떤 모델인지 궁금합니다
저는 MVC 모델 규칙을 최대한 지키며 간단하게 만들려고 노력하고 있어요

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
저 또한 MVC 모델 규칙을 지키면서 미션을 진행하고자 노력하고 있습니다.

다만 저는 애플리케이션의 생명 주기를 Controller에서 관리하는 것은 어울리지 않다고 생각했었습니다.
Controller는 단순히 게임 진행 상황 GameStatus에 따라 View에서 받은 데이터를 토대로 Model에서 로직을 수행하는 것이 전부라고 생각했고, 그 이상으로 책임을 가지는 것은 어색하다고 느꼈습니다.

그래서 Controller는 애플리케이션 사용자의 요청을 전달받고, 그 요청을 처리하고, 요청에 따른 응답을 GameStatus를 반환하도록 했고, 이러한 Controller를 애플리케이션의 생명 주기에 따라 호출하기 위한 유틸리티 클래스가 GameRunner라고 보시면 될 것 같습니다.

Choose a reason for hiding this comment

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

@apptie 아하.. 애플리케이션 생명 주기를 관리하는 변수인 GameStatus를 컨트롤러에서 분리한 것이군요..
객체에 대한 지민님의 고민이 많이 느껴져요 !

gameStatusMappings.put(GameStatus.MAKE_BRIDGE, this::makeBridge);
gameStatusMappings.put(GameStatus.GAME_PLAY, this::gamePlay);
gameStatusMappings.put(GameStatus.GAME_OVER, this::gameOver);
gameStatusMappings.put(GameStatus.GAME_EXIT, this::gameExit);

Choose a reason for hiding this comment

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

저번에 숫자야구 게임에서 설명해주셔서 이해했는데,
이런 방법은 어떻게 떠올리게 되었는지 궁금합니다

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
처음에는 분기문으로 처리했었는데, 그랬더니 메소드가 좀 지저분해지기도 하고, 길어지더라고요...
그래서 해결방법이 있나 구글링을 해보니 분기문을 없애는 다양한 방법이 나와서 그 중 적당한 방식을 선택했었습니다!

Choose a reason for hiding this comment

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

@apptie 아하.. 저도 처음에는 신박한데 복잡해 보인다고 생각했었는데,
애플리케이션이 복잡해질수록 좋은 것 같네요.. 저도 적용해 봐야겠어요


public GameStatus process(GameStatus gameStatus) {
try {
return gameStatusMappings.get(gameStatus).get();

Choose a reason for hiding this comment

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

마지막에 .get()Supplier의 메서드가 맞나요??

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
Supplier의 메소드가 맞습니다!

}

public boolean calculatePlayerMoving(BridgeTile playerStep, PlayerState playerState) {
return playerState.findPlayerPositionTile(Collections.unmodifiableList(bridgeTiles)) == playerStep;

Choose a reason for hiding this comment

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

List를 인자로 넘겨줄 때, Collections.unmodifiableList()로 감싸서 넣어주신 이유가 궁금합니다!

final 키워드처럼 하위의 메서드에서 변경 불가하게 하여 방어적으로 설계하신 의도인가요?!? 이외에 어떤 이점이 있을지 궁금합니다

Copy link
Owner Author

Choose a reason for hiding this comment

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

@HubCreator
말씀하신 대로 방어적인 코드를 위한게 맞습니다!

final의 경우 재할당만이 불가능하기 때문에 List와 같은 컬렉션의 요소를 추가하거나 삭제하는 것이 가능하지만,
unmodifiableList의 경우 외부에서 해당 List를 변경할 경우 예외가 발생하며, 원본 List의 변경에는 영향을 받는 방어적 복사라고 봐주시면 될 것 같습니다!

정리하자면 이점 같은 경우..는 외부에서 변경되어서는 안 될 컬렉션를 넘겨줄 때 변경을 방지하기 위한 목적도 있고, 일급 컬렉션의 불변 객체라는 장점을 살리기 위한 목적도 있다고 봐주시면 될 것 같습니다!

Comment on lines +29 to +31
boolean movable = bridge.calculatePlayerMoving(playerStep, playerState);

playerState.move(movable);

Choose a reason for hiding this comment

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

사전에 움직일 수 있는지의 여부를 반환 받아, move메서드 안에서 조건에 따라 position을 ++하는 방식이 인상깊네요!

저는 코드 상에서 성공인지 실패인지에 따라 직접 분기해주는 것 이외에 방법이 생각나지 않았는데, 배워갑니다!

Comment on lines +10 to +18
public static GameStatus findNextGamePlay(boolean success, boolean movable) {
if (success) {
return GameStatus.GAME_EXIT;
}
if (movable) {
return GameStatus.GAME_PLAY;
}
return GameStatus.GAME_OVER;
}

Choose a reason for hiding this comment

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

게임의 성공과 실패 여부에 따라 달라질 status를 이곳에서 메서드로 결정하는 것이 신기하네요! 책임이 깔끔합니다!

gameStatus = controller.process(gameStatus);
}
}
}

Choose a reason for hiding this comment

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

@apptie 아하.. 애플리케이션 생명 주기를 관리하는 변수인 GameStatus를 컨트롤러에서 분리한 것이군요..
객체에 대한 지민님의 고민이 많이 느껴져요 !

public class IOViewResolver {

private final Map<Class<?>, Consumer<Object>> outputViewMappings = new HashMap<>();
private final Map<Class<?>, Supplier<Object>> inputViewMappings = new HashMap<>();

Choose a reason for hiding this comment

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

이렇게 메서드를 람다식으로 표현하셔서 Map에 넣는걸 좋아하시는 것 같은데,
지민님의 코드 짜는 스타일인지 아니면 뚜렷한 장점이 있는 방법인지 궁금합니다.
저는 한달가량 봤더니 이제서야 좀 적응이 되고 있을 정도로 조금 적응이 안되는 방법이거든요..!ㅎㅎ

Copy link
Owner Author

@apptie apptie Dec 11, 2022

Choose a reason for hiding this comment

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

@eunkeeee
이런 방식은 제 스타일..?이지 않을까 생각합니다.

나름의 이유에 대해 설명드리자면, 제 경우 GameStatus에 따라, 혹은 원하는 타입에 따라 각각을 분기문으로 처리하는 것 보다는 Map에서 GameStatus 혹은 타입을 Key로 갖는 람다식으로 처리하는 것이 더 가독성이 좋을 것이라고 판단했습니다.

또한 방어적인 코드를 작성하고자 할 때, 예외 상황이 발생하는 경우 분기문으로 처리했다면 별도로 예외를 선정하고 그 예외를 던져줘야겠지만, Map으로 표현한다면 NullPointerException이 발생할 것이기 때문에 조금 더 예외 상황 처리를 매끄럽게 할 수 있다고 생각해 이러한 방식을 사용했다고 봐 주시면 될 것 같습니다.

Choose a reason for hiding this comment

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

@apptie 넵 저도 분기문으로 처리하는데 있어서 3주차까지는 괜찮았는데
4주차부터는 index 관리도 안되고 breakcontinue를 포함하게 되면 메서드 분리도 애매해져서
GameStatus같은 게임 전체의 상태를 관리하는 클래스를 사용하기 시작했어요.
물론 분기문 안에서 상태를 관리하는 변수를 update해줘야 하긴 하지만 애플리케이션이 복잡해질수록
지민님 방법이 더 좋은 방법이 될 거 같아요

분기문 대신 map으로 관리해주려면 사실상 모든 과정을 딱 딱 나누어서
서로 전달되어야 하는 변수를 관리하기에 좀 어려울 것 같다는 생각이 들어요.

서로 GameStatus에 대한 정보만 연쇄적으로 전달해
다음 실행할 메서드가 무엇인지 알려주는 정도일 거라고 생각합니다.

같이 사용하는 변수를 필드에 넣는 방법 또는 따로 Variable 같은 클래스를 관리해서
해당 클래스에서 gettersetter를 통해서 수정하고 가져오는 방법이 있을 것 같아요.
(물론 이 Variable 클래스도 필드에 넣어야겠지만요)

저는 분기문을 사용할 때에도 변수를 서로 공유하는 데 있어서
필드가 너무 지저분하게 많아지는 것을 방지하고 싶어서 Variable 클래스를 사용했었는데,

지민님 방법은 이 문제에 있어서 더 리팩토링이 많이 필요할 거 같습니다.
혹시 어떤 노하우가 있는지 여쭤봐도 될까요??


public void outputViewResolve(final Object dto) {
try {
outputViewMappings.get(dto.getClass()).accept(dto);

Choose a reason for hiding this comment

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

outputViewMappingskeySet에 contain되지 않는 dto를 입력했을 때에 여기서 exception을 잡는 것보다 dto 객체들을 인터페이스나 상속 통해서 묶어준 뒤에 인자로 Object가 아닌 그 조상 또는 인터페이스를 넣는 것이 더 다형성을 활용하는 방법일 것 같아요

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
마킹 인터페이스 같은 방식으로 처리하는게 말씀하신 대로 다형성 측면에서는 훨씬 나을 것 같네요. 감사합니다!

private static final String PLAYER_MOVE_DOWN = "D";
private static final String GAME_RETRY = "R";
private static final String GAME_QUIT = "Q";
}

Choose a reason for hiding this comment

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

이부분은 display하는 것을 완전히 view로부터 분리하기 위해서
따로 이곳에 작성한 것인지 궁금합니다!

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
넵 말씀하신 대로 InputView에서 사용자에게 보여줄 메세지와 관련된 내용을 분리하려고 한건데...
enum 안에 넣어놨다고 생각했는데 뭔가 실수했나보네요 ㅎㅎ;

print(GuideMessage.GAME_RETRY.message);

String playerInput = processPlayerInput();

Choose a reason for hiding this comment

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

중간에 줄 공백을 넣는 것을 많이 하시는데,
규칙이 있는지 궁금합니다!

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
가독성을 위해서 제 나름대로의 기준을 가지고 공백을 추가하고 있습니다!

대충 출력 / 변수 할당 / 로직 수행 정도를 공백을 통해 구분하고자 하고 있습니다.

private static final String LINE_FEED = "";

private InputView() {
}

Choose a reason for hiding this comment

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

어떤 분은 InputViewOutputView 내 모든 메서드를 다 인스턴스로 작성해서
Application에서부터 객체를 생성한 뒤에 Controller로 인자를 통해 넘겨주는 방식을 통해서
의존성을 드러난다고 했습니다.

지민님은 Applicaiton은 아니지만
두 인풋 아웃풋 클래스를 관리하는 IOViewResolver
GameRunner에서 컨트롤러로 인자를 통해 전달하고 계시고 있어서 같은 방식이라고 보입니다.

InputViewOutputView에서 모든 메서드를 static으로 관리하는게 어떻다고 생각하시나요??

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
저는 정적 팩토리 메소드와 같이 static이 필요한 경우를 제외하고는 static 사용을 지양해야 한다고 생각합니다.

private enum OutputViewMessage {
APPLICATION_START("다리 건너기 게임을 시작합니다."),
EXCEPTION("[ERROR] %s"),
HISTORY_FORMAT("[ %s ]"),

Choose a reason for hiding this comment

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

저는 앞뒤에 붙이려고 막 StringJoiner하고 그랬는데,
이렇게 간단하게 처리할 수 있었네요...!

@DisplayName("WrongGeneratorException 예외가 발생한다")
void it_throws_exception() {
assertThatThrownBy(() -> bridgeMaker.makeBridge(3))
.isInstanceOf(WrongGeneratorException.class);

Choose a reason for hiding this comment

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

아예 커스텀 Exception을 만들어버리니, 메세지 내용을 테스트하지 않아도 되어서 좋은 것 같네요..

gameStatusMappings.put(GameStatus.MAKE_BRIDGE, this::makeBridge);
gameStatusMappings.put(GameStatus.GAME_PLAY, this::gamePlay);
gameStatusMappings.put(GameStatus.GAME_OVER, this::gameOver);
gameStatusMappings.put(GameStatus.GAME_EXIT, this::gameExit);

Choose a reason for hiding this comment

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

@apptie 아하.. 저도 처음에는 신박한데 복잡해 보인다고 생각했었는데,
애플리케이션이 복잡해질수록 좋은 것 같네요.. 저도 적용해 봐야겠어요

public String findFullMessage(Object... replaces) {
return String.format(baseMessage, replaces);
}
}

Choose a reason for hiding this comment

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

그때 고민하신 문어발식 import를 막기 위해서 ExceptionMessage를 해당 클래스에서 관리하시기로 한 건가요?!?

Copy link
Owner Author

@apptie apptie Dec 11, 2022

Choose a reason for hiding this comment

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

@eunkeeee
넵 맞습니다!
정확히는 아직 어떤 방식이 더 나은지 잘 감이 안와서, 일단 둘 다 써보자 싶어서 한 번 써봤습니다.
그런데 아직은 뭐가 더 나은지 잘 판단이 안되서 조금 더 이런 방식을 사용하려고 합니다 ㅎㅎ;

return Arrays.stream(GameCommand.values())
.filter(gameCommand -> gameCommand.command.equals(command))
.map(gameCommand -> gameCommand.gameStatus)
.findAny()

Choose a reason for hiding this comment

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

여기서 (어차피 같은 것이 여러개 있는 상황이 아니니)
저는 항상 findFirst()를 사용할지 findAny()를 선택할지 고민되어서
일관성 없이 사용하곤 하는데
지민님이 findAny()를 사용하는 이유가 궁금합니다

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
큰 이유는 없고.. 말씀하신 대로 같은 것이 여러 개 있는 상황이 아니기 때문에 아무거나 찾자 -> 아무거나 -> findAny와 같이 의식의 흐름대로..? 사용했었습니다 ㅎㅎ;

Choose a reason for hiding this comment

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

@apptie 아 ㅋㅋㅋㅋ 의식의 흐름 너무 재밌네욬ㅋㅋ 답변 감사드려요

String gameCommand = readGameCommandDto.getGameCommand();
GameStatus nextGameStatus = GameCommand.findNextGameOver(gameCommand);

if (nextGameStatus == GameStatus.GAME_PLAY) {

Choose a reason for hiding this comment

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

이런 간단한 비교는 바로 ==으로 비교하는 것이 객체 관점에서 괜찮을지 요즘 고민중입니다.
이런것까지 GameStatus에 보내서 클래스에
isGamePlay()같은 메서드를 만드는 것도 좀 지저분해 보이기도 하구요...

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
저도 동일한 고민을 지금까지 하고 있습니다.

요즘은 enum 외부에서 직접 비교하는 것 보다는 말씀하신 대로 enum 내부에서 메소드를 제공하는게 맞지 않나 싶기도 해서...
일단은 대충 편한대로 했는데, 아직도 고민이 되네요 ㄷㄷ..

Choose a reason for hiding this comment

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

@apptie 네 확실히 지민님이 자주 사용하시는 playable()은 왜인지 enum에서 확실히 관리해야 할 거 같은데
나머지의 경우에는 저도.. 매번 만들기도 귀찮아서 == 그대로 남아있는 경우도 많아서
일관성 측면에서 고민이 많습니다 ...ㅎㅎ

public enum MovingStep {
CORRECT_MOVE("O"),
WRONG_MOVE("X"),
NOT_MOVE(" ");

Choose a reason for hiding this comment

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

저도 이렇게 사용했었는데, 한 분께 이 부분은 콘솔에 표시하는 내용이므로
display는 확실하게 view로 분리하는 것이 깔끔하다는 피드백을 받았었습니다.
지민님 생각도 궁금합니당

Copy link
Owner Author

@apptie apptie Dec 11, 2022

Choose a reason for hiding this comment

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

@eunkeeee
생각해보니 저도 View로 분리하거나 toString을 통해 표현하는 등 다른 방법을 사용하는 게 조금 더 좋은 방법인 것 같습니다. 감사합니다!


private String mapToPlayerStepHistoryLog(List<MovingStep> targetHistory) {
return targetHistory.stream().map(MovingStep::toString).collect(Collectors.joining(HISTORY_SEPARATOR));
}

Choose a reason for hiding this comment

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

마찬가지로 이부분도 display와 관련도니 부분이니 domain의 역할이 아닌 것 같기도 합니다.

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
이 부분은 확실히 toString을 통한 변환을 View에서 작업하는게 맞겠네요. 감사합니다!


public void clear() {
this.upStepHistory.clear();
this.downStepHistory.clear();

Choose a reason for hiding this comment

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

Listclear() 메서드를 사용하셨는데,
현재 upStepHistorydownStepHistory가 static으로 관리되고 있지 않으니,
아예 MovingStepHistory 객체를 새로 생성하는 방법도 있을 것 같은데,
현재의 방법이 더 효율적이라고 생각하신 이유가 있나요?

Copy link
Owner Author

Choose a reason for hiding this comment

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

@eunkeeee
게임을 다시 시작할 때 마다 매번 새로운 인스턴스를 생성하며 GC의 대상을 추가하는 것보다는 한 번 만들어놓은 인스턴스를 재활용 하는 것이 더 효율적이라고 생각했습니다!

Choose a reason for hiding this comment

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

@apptie 위에서부터 읽다보니 제가 질문이 너무 많았네요....ㅋㅋㅎ...
다 친절하게 답변주셔서 감사드려요!!

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.

3 participants