Conversation
- Participant 객체 없음
- 이름이 5글자 이상일 경우, Exception 발생 O - 이름이 5글자 이하인 경우, Exception 발생 X
- Winning 객체 없음
- Point 객체는 사다리의 선과 선의 교점을 나타낸다.
- Line 객체는 Point가 한줄 모였을 때를 나타낸다.
- Ladder 객체는 참가자와 높이 수만큼 Point의 집합니다.
- participant 크기만큼 초기화한 line을 생성 - 생성한 line을 height만큼 Ladder ArrayList에 추가
- Q: parseToParticipant와 parseToWinning의 반환값만 다르고 같은 일을 한다. 중복되는 부분을 줄일 수 있는 방법이 있을까?
- 사다리를 구성하는 한줄(참가자 크기 사이즈)을 생성
- Line getLadder(int i) 메서드 삭제: Ladder를 구성하는 line 반환 / List<Line> getLines() 메서드 생성: Ladder를 구성하는 line의 모음을 반환
- Info 클래스를 상속하여 사용
- asList() 사용하여 변환 테스트
| generatedStringLadder.add(toStringPlayerList(players)); | ||
|
|
||
| for (Line value : lines) { | ||
| ArrayList<String> generatedLine = new ArrayList<>(); |
There was a problem hiding this comment.
모든 타입 선언은 구체클래스가 아닌 인터페이스로 통일되면 좋을 것 같습니다.
지난번에 공유드린 이펙티브 자바 아이템을 참고하시면 좋을 것 같아요 ~
|
|
||
| for (Line value : lines) { | ||
| ArrayList<String> generatedLine = new ArrayList<>(); | ||
| Line line = value; |
| if (!(line.getLine().get(j)).getConnection()) { | ||
| generatedLine.add(EMPTY); | ||
| } | ||
| if (line.getLine().get(j).getConnection()) { |
There was a problem hiding this comment.
26번째 줄에서도 공통으로 호출되고 있는 부분인데 변수로 빼면 어떨까요?
그리고 getConnection 보다는 이어져있는지 판별하는 메서드명이 어울리지 않을까 싶습니다
ex isConnected()
| import java.util.List; | ||
|
|
||
| public class LadderGame { | ||
| public Ladder generateLadder(int height, int participant) { |
There was a problem hiding this comment.
LadderGame 은 상태를 가지지 않는 클래스인 것 같아보이네요. 입력받은 파라미터들로 메서드 내부의 로직으로만 반환값을 만들 수 있다면 클래스로 따로 빼는게 좋을지? 아니면 다른 책임으로 옮겨갈 여지가 있을지 고민해보시는 것도 좋을 것 같습니다
src/main/java/model/Height.java
Outdated
| import exception.HeightIsUnderMiniHeightException; | ||
|
|
||
| public class Height { | ||
| private static final int MINI_HEIGHT = 1; |
src/main/java/model/Info.java
Outdated
| @@ -0,0 +1,17 @@ | |||
| package model; | |||
|
|
|||
| public class Info { | |||
There was a problem hiding this comment.
Info 라는 클래스는 매우 추상적인 클래스로 보이는데요, 따로 만드신 이유가 있을까요?
There was a problem hiding this comment.
@hyukjin-lee
Info 클래스는 Player와 Winning 클래스에서 상속해서 사용하는데요~
Player와 Winning의 정보를 입력받아 List 형식으로 만들어 반환하는 기능을 구현할 때, Player와 Winning을 래핑해서 사용하다보니 메서드의 동작은 같지만 반환값이 각각 List<Player>, List<Winning>으로 다른 두개의 메서드가 작성되었습니다.
중복을 줄일 수 있는 방법이 무엇일지 고민하다 상속이 떠올랐고 상속할 클래스로 Info를 만들었습니다.
|
|
||
| import java.util.List; | ||
|
|
||
| public class Ladder { |
There was a problem hiding this comment.
요구사항으로인해 일급컬렉션을 만들고 값들을 다 래핑하신 것 같은데, 실제로 그것의 장점이 반영되지 못한 것 같습니다.
Ladder 클래스에 위임될만한 책임이 없을지 고민해보셔도 좋을 것 같아요. 지금은 생성자로 생성하고 getter 로 호출하는 것 뿐이어서 일급컬렉션의 의미가 퇴색되지 않나해서요 ~ :)
There was a problem hiding this comment.
@hyukjin-lee
'상태와 행위를 한 곳에서 관리할 수 있다'는 일급 컬렉션의 장점을 좀더 살려보라는 것으로 이해해도 될까요?
|
|
||
| public class Line { | ||
| private static final int NON_CONNECTION = 0; | ||
| private static final int CONNECTION = 1; |
There was a problem hiding this comment.
boolean 값으로 표현하면 어떨까요? 숫자로 표현하신 이유가 있을지 궁금합니다 ~
There was a problem hiding this comment.
private boolean isVisited() {
Random random = new Random();
int randomNumber = random.nextInt(BOUND_NUMBER);
방문 여부를 숫자를 사용하여 랜덤으로 지정하려고 표현하였습니다~
| } | ||
|
|
||
| private boolean isVisited() { | ||
| Random random = new Random(); |
There was a problem hiding this comment.
@hyukjin-lee
제 생각은 값이 랜덤한가를 테스트하는 것은 어렵다고 생각합니다. 또 테스트 코드를 작성하는 의도가 자바의 라이브러리를 테스트하는 것이 아닌 제가 작성한 코드를 테스트하는 것이라고 생각합니다.
그래서 랜덤으로 나온 값을 호출하는 것을 테스트하는 것이 필요하다고 생각합니다.
여기서는 random으로 생성된 randomNumber가 제가 생각하는 범위(BOUND_NUMBER)안의 숫자이고, 타입(int)이 올바른지 파악하고 싶습니다.
src/main/java/model/Player.java
Outdated
|
|
||
| import exception.PlayerNameIsOverMaxNameSizeException; | ||
|
|
||
| public class Player extends Info { |
There was a problem hiding this comment.
info 를 만들어서 상속시킨 이유가 궁금합니다 ~
그리고 상속과 조합의 차이점에 대해서 공부해보셔도 좋을 것 같아요 :)
There was a problem hiding this comment.
@hyukjin-lee
Player와 Winning에 해당하는 값을 입력받아 가공할 때 둘 사이에 String 타입의 파라미터로만 이루어졌다는 공통점이 있어, 동일한 로직이 타입만 다르게 중복되었습니다.
중복을 줄일 수 있는 방법이 무엇일지 고민하다 상속이 떠올랐고 상속할 클래스로 Info를 만들었습니다.
피드백을 반영하고 리서치하다보니 상속이 적합하지 않다는 생각이 들었습니다.
상속은 '포유류(동물을 상속)는 동물이다.' 처럼 확실한 is-a 관계여야하는데 'Player는 정보다' 정보가 매우 추상적이고 자손인 Player와 Winning 사이에도 확실한 연관관계가 없기 때문입니다.
따라서 이를 해결할 수 있는 방법을 고민해보았습니다.
- Player와 Winning을 공통으로 하는 클래스를 생성해서 이를 각각 Player와 Winning으로 재정의하자.
Player와 Winning의 공통된 부분을 Info라고 한다면Info Player;,Info Winning;처럼 입니다. 코멘트를 반영하다보니 LadderGame에서 전체적인 흐름을 관리하게 되었고 LadderGame을 생성할 때 값을 검증해도 되겠다고 생각했습니다.
하지만 이 경우에 Player와 Winning에 추후에 기능이 추가되면 코드를 전체적으로 수정해야하므로 이 방법은 적절하지 않다고 생각했습니다.
-
인터페이스를 사용한다.
이 경우에는 인터페이스 사용이 상속으로 구현한 것과 차이가 없다고 생각해서 적절하지 않다고 생각했습니다. -
조합 사용
말씀하신 조합을 공부하게 되었고, StringInfo를 변수로 가지도록 코드를 수정했습니다.
+) 다만, WinningLotto에서 BonusBall이나 Lotto 클래스를 조합하여 사용할 경우 관계가 확실한테 제 경우에는 Player와 StringInfo, Winning과 StringInfo의 관계가 추상적이라고 생각합니다. 문제는 변수명이라고 생각하는데 마땅히 떠오르지 않네요😅
src/main/java/model/Winning.java
Outdated
| public class Winning extends Info { | ||
| private static final int MINIMUM_NUMBER_SIZE = 0; | ||
| private static final String LOSING_TICKET = "꽝"; | ||
| private static final String numberPattern = "^[0-9]+$"; |
| int testHeight = -1; | ||
|
|
||
| // when & then | ||
| assertThatThrownBy(() -> new Height(testHeight)) |
|
|
||
| public class HeightTest { | ||
| @Test | ||
| public void should_throwException_when_height_is_under_1() { |
There was a problem hiding this comment.
JUnit 5 는 테스트에 접근제어자를 붙이지 않아도 됩니다 ~
그리고 @DisplayName 으로 테스트의 목적을 표현해보면 어떨까요? (~인 경우 ~할 때 ~ 한다)
| import static org.assertj.core.api.Assertions.*; | ||
|
|
||
| public class WinningTest { | ||
| @ParameterizedTest |
build.gradle
Outdated
| dependencies { | ||
| testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' | ||
| testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' | ||
| implementation 'org.testng:testng:7.1.0' |
There was a problem hiding this comment.
@hyukjin-lee
정확한 이유는 모르겠으나 사용하고 있지 않은 것을 보니 잘못들어간 것 같습니다.
덕분에 찾아보니 JUnit과 NUnit에서 나온 자바 테스트 프레임워크라고 합니다.
Junit과 차이를 보자면
Annotations
JUnit 5는 테스트 클래스를 초기화하거나, 테스트 후 실행시킬 때 @BeforeAll과 @afterall을 사용해야 합니다.
TestNG는 그럴 필요 없이 사용할 수 있습니다. 사용할 수 있는 어노테이션은 다음과 같습니다.
@BeforeSuite, @AfterSuite, @BeforeTest, @AfterTest, @BeforeGroup, @aftergroup, @BeforeMethod, @AfterMethod
selenium을 사용한 자동 테스트 앱을 만드는데도 사용된다고 하네요. (참고)
Parameterized testing
모두 Parameterized 테스트가 가능합니다. TestNG는 testng.xml이나 @dataProvider 어노테이션을 사용해서 할 수 있습니다.
결론은 JUnit이 더 많이 사용되고, 기본적으로 IDE와 함께 제공됩니다. 하지만 TestNG는 다양한 테스트를 할 수 있는 추가 옵션들이 있으니 용도에 맞게 선택해서 사용하면 될 것 같습니다.
+) IntelliJ IDEA에서 TestNG를 사용 방법에 대한 아티클이 있습니다.
테스트 결과를 HTMl 파일로 직관적으로 확인할 수 있는게 매력적인 것 같네요~
Ref
build.gradle
Outdated
| testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' | ||
| implementation 'org.testng:testng:7.1.0' | ||
| testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' | ||
| testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' |
There was a problem hiding this comment.
@hyukjin-lee 여러 argument를 이용해 테스트를 여러번 돌릴 수 있는 테스트를 할 수 있는 기능인 Junit5의 Parameterized Test를 사용하기 위한 설정입니다!
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-setup
build.gradle
Outdated
| implementation 'org.testng:testng:7.1.0' | ||
| testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' | ||
| testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' | ||
| testCompile 'org.assertj:assertj-core:3.11.1' |
There was a problem hiding this comment.
testCompile 과 testImplementation 의 차이점은 뭘까요?
There was a problem hiding this comment.
@hyukjin-lee
compile과 implementation의 차이를 살펴보겠습니다.
compile: 모듈을 수정할 때, 직접 혹은 간접 의존하고 있는 모듈을 모두 재빌드한다.implementation: 모듈을 수정할 때, 직접 의존하고 있는 모듈만 재빌드합니다.
찾아보니 compile은 이제 deprecated되었고, api로 대체되었네요...!
둘 차이를 봤을 때, api를 사용하면
- Module의 Library의 인터페이스까지 노출됩니다.
- 클린아키텍쳐 측면에서도 각 레이어별로 분리가 되어야하는데
api를 사용하면 모듈 계층을 건너뛴 참조가 발생할 수 있어 레이어를 나눈 이유가 없습니다.
이런 이유로 Implementation으로 수정했습니다.
참고:
49a9378 to
b106b3b
Compare
| // DESC: left, right 갈 곳이 없다 -> Y축 이동한다. | ||
| if (!direction.isLeftFlag() && !direction.isRightFlag()) { | ||
| playerPosition.moveYAxis(playerPosition.getY() + 1); | ||
| playerPoint = ladder.getLines().get(playerPosition.getY()).getPoints().get(playerPosition.getX()); |
There was a problem hiding this comment.
개인적으로는 체이닝 메서드에 대한 개행을 사용하면 조금 더 보기 좋은 것 같더라구요! 혹시나 필요하시면 아래 순서 따라 들어가서 설정해보시면 좋을 것 같습니다!
Settings -> Editor -> Code Style -> Java
Wrapping and Braces 탭에서 ChainedMethod calls 부분 설정 변경
There was a problem hiding this comment.
@saint6839 오~ 감사합니다!!
+) 중요한 것은 아닌데 메서드 체이닝(Method chaining)이라는 이름으로 더 부르는 것 같습니다! 혹시 몰라 공유합니다.
src/test/java/model/PlayerTest.java
Outdated
|
|
||
| public class PlayerTest { | ||
| @Test | ||
| public void should_throwException_when_participantName_over_5() { |
There was a problem hiding this comment.
camel case와 snake case가 중복 사용된 것 같습니다!
- WinningSize와 PlayerSize 크기 비교
사다리 생성 방법
+) 원래 생각했던 방법
사다리를 바둑판으로 보고 line을 세로로 두고 가로로 이동하면서 point를 지정하려고 했습니다. 저는 계산이 무척 복잡해지던데(부분집합이 계속 생기고 이전 line과 비교하면서...) 이런식으로 접근하신분도 계신가요?
어려운 점
함수 반복을 줄이는 방법
동작하는 내용은 같은데 반환값이 달라 두번 작성하는 경우가 있었습니다. 반복을 줄일 수 있는 방법이 있을까요? -> 상속을 이용해보았습니다.
패키지 분리
작성한 코드를 mvc 패턴을 적용하여 패키지 분리를 하였습니다.
적절하게 분리가 되었는지 피드백 주시면 감사하겠습니다~