깃 저장소
https://github.com/ljw1126/my-java-coordinate-playground/tree/practice/src/main/java
복습 - 좌표 계산기 (선, 사각형, 삼각형)
[요구사항]
① 사용자가 점에 대한 좌표 정보를 입력한다
② 좌표 정보는 괄호로 둘러쌓여 있으며, 쉼표(,) 로 x/y값을 구분한다
[예시]
선의 경우 (10,10)-(14,15)
사각형의 경우 (10,10)-(22,10)-(22,18)-(10,18)
삼각형의 경우 (10,10)-(14,15)-(20,8)
③ x, y 좌표 값의 범위는 1 ~ 24 이다
④ 입력 범위를 초과할 경우 에러 문구를 출력하고 다시 입력을 받는다
⑤ 정상적인 값을 입력한 경우, 콘솔에 2차원 그래프와 결과값을 출력한다
[결과]
선의 경우 두 점 사이의 거리를 계산
사각형의 경우 넓이 계산
삼각형의 경우 넓이 계산
1. 좌표 계산기(선) 구현하기
[기능 정리]
① Point 클래스를 생성하고, X, Y 좌표값을 저장한다
- 각 포인트가 1 ~ 24 사이의 값인지 유효성 검사한다
② Line 클래스 생성하고, Point 정보를 저장한다
- Point 의 개수가 2인지 확인한다
- 두 점의 사이의 거리를 구한다 (위키 참고. 두 점 사이의 거리)
- 결과 형식을 반환한다
③ InputView 클래스를 생성한다
- 정규 표현식으로 사용자 입력값이 유효한지 확인한다
- 정규 표현식으로 좌표 값을 파싱하여 반환한다
④ OutputView 클래스를 생성한다
- Point 정보를 받아 2차원 좌표와 점을 그린다
- 마지막 줄에 계산 결과를 출력한다
#Point 클래스
테스트 작성
class PointTest {
@Test
void exception() {
assertThatThrownBy(() -> new Point(0, 1))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new Point(24, 25))
.isInstanceOf(IllegalArgumentException.class);
}
}
클래스 생성
public class Point {
private static final int MIN_VALUE = 1;
private static final int MAX_VALUE = 24;
private static final String INVALID_POINT = String.format("포인트 값은 %s ~ %s 사이의 값이여야 합니다", MIN_VALUE, MAX_VALUE);
private int x;
private int y;
public Point(int x, int y) {
if(invalidRange(x) || invalidRange(y)) {
throw new IllegalArgumentException(INVALID_POINT);
}
this.x = x;
this.y = y;
}
private boolean invalidRange(int point) {
return point < MIN_VALUE || MAX_VALUE < point;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
#Line 클래스
테스트 작성
class LineTest {
@Test
void exception() {
List<Point> points = Arrays.asList(new Point(10, 10), new Point(14, 15), new Point(24, 24));
assertThatThrownBy(() -> new Line(points))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void calculateDistance() {
List<Point> points = Arrays.asList(new Point(10, 10), new Point(14, 15));
Line line = new Line(points);
double distance = line.calculateDistance();
double expected = 6.4;
assertThat(distance).isEqualTo(expected, Offset.offset(0.1));
}
}
Line 클래스 기능 구현
Point 컬렉션 정보를 관리하는 Points (Wrapper) 클래스를 추가로 생성했다
public class Line {
private static final int POINT_SIZE = 2;
private static final String INVALID_POINT_SIZE = String.format("선을 그리기 위한 점은 %s개가 필요합니다", POINT_SIZE);
private static final String RESULT_PREFIX = "두 점 사이 거리는 ";
private Points points; // Wrapper 클래스
public Line(List<Point> points) {
if(points.size() != POINT_SIZE) {
throw new IllegalArgumentException(INVALID_POINT_SIZE);
}
this.points = new Points(points);
}
public double calculateDistance() {
double differenceX = points.getDifferenceX();
double differenceY = points.getDifferenceY();
return Math.sqrt(pow(differenceX) + pow(differenceY));
}
private double pow(double value) {
return Math.pow(value, 2);
}
public String result() {
return RESULT_PREFIX + calculateDistance();
}
}
추가적으로 리팩토링할 부분이 보이지 않아 다음으로 넘어간다
#입력 데이터 처리
- 처음에는 입력받은 String 문자열을 split() method 를 활용하는 방식으로 구현했으나, 너무 지저분해보였다
- 찾아 본 결과 정규 표현식과 Pattern, Matcher 로 유효성 검사와 데이터 파싱 가능하여 이 방식으로 변경하였다
참고.
https://coding-factory.tistory.com/529
테스트 코드 작성
- \d : digit, [0-9]와 같다
- {1, 2} : 1개 또는 2개가 있다, 반복 횟수 나타냄
- \d{1,2} : 숫자가 1개 또는 2개가 있다
public class RegexpTest {
private String lineInput = "(10,10)-(14,15)";
@Test
void 선입력_매칭() {
String regexp = "\\(\\d{1,2},\\d{1,2}\\)-\\(\\d{1,2},\\d{1,2}\\)";
Pattern pattern = Pattern.compile(regexp);
Matcher matcher = pattern.matcher(lineInput);
assertThat(matcher.find()).isTrue();
}
@Test
void 선입력_파싱() {
String regexp = "\\((\\d{1,2}),(\\d{1,2})\\)";
Pattern pattern = Pattern.compile(regexp);
Matcher matcher = pattern.matcher(lineInput);
List<Point> actual = new ArrayList<>();
while(matcher.find()) {
int x = Integer.parseInt(matcher.group(1));
int y = Integer.parseInt(matcher.group(2));
actual.add(new Point(x, y));
}
List<Point> expected = Arrays.asList(new Point(10, 10), new Point(14, 15));
assertThat(actual).isEqualTo(expected);
}
}
InputView와 OutputView는 저장소 코드 참고
https://github.com/ljw1126/my-java-coordinate-playground/tree/practice/src/main/java
2. 좌표 계산기(사각형) 구현하기 & 리팩토링
[기능 정리]
Rectangular 클래스를 생성한다
- 입력 좌표값의 유효성 검사를 한다
- 넓이를 계산한다
- 결과 형식을 반환한다
#리팩토링1 - 클래스 구조 변경
직사각형이 추가되면서 아래와 같은 문제가 발생하여 구조를 변경할 필요성을 느꼈다
public class CoordinateController {
public void run() {
InputView inputView = new InputView();
OutputView outputView = new OutputView();
//Line line = new Line(inputView.input());
Rectangular rectangular = new Rectangular(inputView.input());
outputView.draw(rectangular.getPoints());
outputView.showResult(rectangular.result());
}
}
Line과 Rectangular 클래스의 공통점은 아래와 같았다
① 좌표값을 가진다 -- 속성
② 계산하여 결과값을 구한다 -- 행위
③ 결과 형식을 반환한다 -- 행위
그래서 공통 부분을 줄이기 위해 상속만을 사용하는 것을 처음 생각했었다
하지만 과제 레벨에서 충분할 수도 있지만, DIP 원칙에서 봤을 때 좋은 형태는 아닌거 같았다
"추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 만든다" - Head First Design Pattern 도서
그래서
인터페이스(Figure)를 두고 추상 클래스(AbstractFigure)를 상속받아 각 구현체별 기능 구현하게 된다면
캡슐화도 되고, 한 군데만 수정하면 된다 생각하여(SRP 원칙 준수) 아래와 같이 변경하였다
Figure 인터페이스 생성
public interface Figure {
Points getPoints();
double getArea();
String getAreaInfo();
}
AbstractFigure 추상 클래스 생성
public abstract class AbstractFigure implements Figure {
protected final Points points;
protected AbstractFigure(List<Point> points) {
this.points = new Points(points);
}
@Override
public Points getPoints() {
return this.points;
}
}
Line 클래스
public class Line extends AbstractFigure {
public static final int POINT_SIZE = 2;
private static final String INVALID_POINT_SIZE = String.format("선을 그리기 위한 점은 %s개가 필요합니다", POINT_SIZE);
private static final String RESULT_PREFIX = "두 점 사이 거리는 ";
public Line(List<Point> points) {
super(points);
if(points.size() != POINT_SIZE) {
throw new IllegalArgumentException(INVALID_POINT_SIZE);
}
}
@Override
public double getArea() {
// 구현
}
@Override
public String getAreaInfo() {
// 구현
}
}
#리팩토링2 - 정적 팩토리 메서드 사용
- InputView 에서 Figure 인터페이스 형식을 반환하도록 수정했는데 아래와 같이 조건문이 지저분한게 나타났다
- 힌트를 얻어 디자인 패턴 중 객체 생성과 관련된 정적 팩토리 메소드를 적용하여 처리하였다
public class InputView {
// 필드..
public Figure input() {
System.out.println(INPUT_MESSAGE);
Scanner sc = new Scanner(System.in);
return process(sc);
}
private Figure process(Scanner sc) {
try {
String input = sc.next();
invalidCoordinateInput(input);
List<Point> points = parsePoints(input);
// 여기!
if(points.size() == Line.POINT_SIZE) {
return new Line(points);
}
if(points.size() == Rectangular.POINT_SIZE) {
return new Rectangular(points);
}
return null;
} catch (RuntimeException e) {
System.out.println(e.getMessage());
return input();
}
}
}
FigurFactory 클래스 생성 - 정적 팩토리 메소드
public class FigureFactory {
private static final String DUPLICATE_POINT = "중복된 좌표가 포함되어 있습니다";
private static final Map<Integer, Function<List<Point>, Figure>> createMap;
static {
createMap = new HashMap<>();
createMap.put(Line.POINT_SIZE, Line::new);
createMap.put(Rectangular.POINT_SIZE, Rectangular::new);
}
public static Figure create(List<Point> points) {
checkDuplicatePoint(points);
return createMap.get(points.size()).apply(points);
}
private static void checkDuplicatePoint(List<Point> points) {
if(points.size() != new HashSet<>(points).size()) {
throw new IllegalStateException(DUPLICATE_POINT);
}
}
}
수정후
public class InputView {
// 필드, 생성자, 메소드
private Figure process(Scanner sc) {
try {
String input = sc.next();
invalidCoordinateInput(input);
List<Point> points = parsePoints(input);
return FigureFactory.create(points); //*여기
} catch (RuntimeException e) {
System.out.println(e.getMessage());
return input();
}
}
}
3. 좌표 계산기(삼각형) 구현하기 & 리팩토링
[기능 정리]
-삼각형 넓이 구하기 (위키. 헤론의 공식 활용)
-좌표 유효성 검사 (위키. 기울기 공식 활용)
AbstractFigure 클래스 상속받아 기능 구현, 테스트 하면 되므로 자세한 내용 생략
#리팩토링 - Points Wrapper 클래스 제거
처음 여러 좌표 값을 한 군데에서 담당하기 위해 Points (일급 컬렉션) 클래스를 생성하여 한 군데서 관리하도록 의도하였다
public class Points {
private List<Point> points;
// 생성자, 관련 함수
}
그런데 결과값 계산시 삼각형에서 본의 아니게 getter를 통해 컬렉션을 꺼내 사용하거나 (ex. this.points.getPoints().getX())
좌표값(원시값)을 직접 꺼내 연산 하는 형태가 좋지 않아 보였다 (위키 참고. 최소 지식 원칙, 디미터 원칙)
"객체와 객체를 비교하는게 객체 지향 사고이다" - 자바지기
그래서 되도록 메소드 체인을 줄이고, 객체에게 메시지를 보내는 형태로 변경을 해보았다
Points 클래스 삭제
List<Point> points 를 AbstractFigure 클래스로 이동
두 Point 객체 비교시 인스턴스 메소드 사용하도록 변경 (ex. 두 점 사이 거리 계산)
AbstractFigure 클래스
public abstract class AbstractFigure implements Figure {
protected final List<Point> points;
public AbstractFigure(List<Point> points) {
this.points = points;
}
@Override
public List<Point> getPoints() {
return this.points;
}
@Override
public boolean hasPoint(int x, int y) {
return this.points.stream()
.anyMatch(point -> point.isSame(x, y));
}
}
Line 클래스 - 계산 처리 부분
public class Line extends AbstractFigure{
// 필드, 생성자
@Override
public double getArea() {
//Point 클래스의 인스턴스 메소드로 처리
return this.points.get(0).calculateDistance(this.points.get(1));
}
}
Rectangular 클래스 - 계산 처리 부분
public class Rectangular extends AbstractFigure {
// 필드, 생성자..
@Override
public double getArea() {
double width = calculateDifference(distinctPoint(this.points, Point::getX));
double height = calculateDifference(distinctPoint(this.points, Point::getY));
return width * height;
}
private double calculateDifference(List<Integer> points) {
return Math.abs(points.get(0) - points.get(1));
}
private List<Integer> distinctPoint(List<Point> pointList, Function<Point, Integer> mapFunction) {
return pointList.stream()
.map(mapFunction)
.distinct()
.collect(toList());
}
}
Triangle 클래스 - 계산 처리 부분
public class Triangle extends AbstractFigure {
// 필드, 생성자..
@Override
public double getArea() {
double a = distance(this.points.get(0), this.points.get(1));
double b = distance(this.points.get(0), this.points.get(2));
double c = distance(this.points.get(1), this.points.get(2));
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
private double distance(Point pointA, Point pointB) {
return pointA.calculateDistance(pointB);
}
}
회고
3주만의 복습이었는데, 대략 4시간 정도 소요된거 같다
(처음 구현시 중간중간 컷닝을 하고도 4일은 걸렸던 것으로 기억한다.. 🥹)
개인적으로 힘들었던 부분은 아래와 같다
① 정규식과 Pattern, Matcher 클래스 활용하여 좌표값 파싱 처리
② OutputView에서 2차원 그래프 그리기
③ TDD 사이클 지키기
④ 객체 지향적 코드 작성하기 (ex. 객체와 객체를 비교하거나 메시지 보내기)
⑤ Wrapper 클래스 남용
특히나 Points (일급 컬렉션) 클래스를 만들고 사용하면서
get..().get..()과 같은 메소드 체인을 어떻게 단순화할 지 생각하지 못한게 아쉬웠다
keyword : 디미터 원칙, 최소 지식 원치, 응집도
업무에서 부족하면 필요한게 더 없는지,
과하면 뺄게 없는지 고민을 자주 했었는데
코드를 바라보는 시각이 훈련이 덜 되서
반영이 되지 않는거 같다
잘 하고 싶다
'공부 > Java' 카테고리의 다른 글
[넥스트스탭] 블랙잭 - 자바 플레이 그라운드 (0) | 2024.04.30 |
---|---|
[넥스트스탭] 자동차 경주 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
[넥스트스탭] 숫자 야구 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
[Java] Reflection API 테스트 학습(with Baeldung) (0) | 2023.11.20 |
[Java] Stream Quiz 개인 풀이 (출처. 망나니 개발자 기술 블로그) (0) | 2023.08.15 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!