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

[1단계 - 장기] 차니(이동찬) 미션 제출합니다. #71

Merged
merged 46 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
03c73ec
docs: 기능 목록 정리
ppparkta Mar 18, 2025
4726844
feat: 좌표 관련 모델 구현
DongchannN Mar 18, 2025
0bb6140
refactor: 매직넘버 제거
ppparkta Mar 18, 2025
b35e6b2
feat: 수직,수평인지 판단한다
ppparkta Mar 18, 2025
7c3cc88
feat: 차의 이동을 구현한다
ppparkta Mar 18, 2025
248a6bc
test: 수직, 수평 테스트
ppparkta Mar 18, 2025
d0fd565
feat: 궁, 사 기물을 구현한다
ppparkta Mar 18, 2025
da4566c
style: 개행 규칙
ppparkta Mar 18, 2025
a1f8ee3
feat: 장기말 세팅
ppparkta Mar 19, 2025
fb95774
docs: 기능 구현 목록 업데이트
ppparkta Mar 19, 2025
9443472
feat: "차" 기물이 가질 수 있는 모든 경로 탐색 로직 구현
ppparkta Mar 19, 2025
7d0593d
feat: "졸"의 이동 가능한 모든 경로 구현
ppparkta Mar 19, 2025
1e03a13
feat: "마"의 이동 가능한 모든 경로 구현
ppparkta Mar 19, 2025
4f75b0b
feat: "상"의 이동 가능한 모든 경로 구현
ppparkta Mar 19, 2025
cbe9296
feat: "포"의 이동 가능한 모든 경로 구현
ppparkta Mar 19, 2025
5e96303
feat: try-catch 추가
ppparkta Mar 19, 2025
0dcc7c8
feat: 엔드포인트에 턴과 동일한 팀의 기물이 있으면 경로 제거
ppparkta Mar 19, 2025
9855aaa
refactor: 장기판 초기화 로직 간소화
ppparkta Mar 19, 2025
7a14f39
feat: 포 이동 가능한 구간 확인
ppparkta Mar 20, 2025
6aba16d
refactor: Janggi 클래스 메서드 분리
ppparkta Mar 20, 2025
52dc337
refactor: 기물 종류 열거형 분리
ppparkta Mar 20, 2025
d710e96
refactor: 패키지 분리
ppparkta Mar 20, 2025
0d38e84
refactor: "상"과 "마" 이동 로직 간소화
ppparkta Mar 20, 2025
caa798b
refactor: 디랙토리 정리
ppparkta Mar 20, 2025
45f304f
feat: view 구현
ppparkta Mar 20, 2025
496e4ba
feat: 장기 게임의 한 개 Turn 구현
ppparkta Mar 20, 2025
91f2625
feat: 사용자 콘솔 인터페이스 구현
ppparkta Mar 20, 2025
5c7b98b
docs(README.md) : 기능 요구사항 정리 및 피드백 반영 예정사항 작성
DongchannN Mar 23, 2025
ab79815
refactor: 장기 기획서를 준수하는 네이밍으로 변경
DongchannN Mar 23, 2025
3c8af9b
refactor: 컨벤션 반영 및 불필요한 메서드 제거
DongchannN Mar 23, 2025
211d248
refactor: View 출력 계산 시 if문에 도달하지 않는 경우에 대해 예외처리
DongchannN Mar 23, 2025
f259b0d
refactor: 기존 Janggi의 턴 수정 방식을 Janggi와 Team 사이의 메시지 전달로 수정
DongchannN Mar 23, 2025
5a6af00
refactor: Unit이 Position을 관리했던 책임을 Janggi로 이관
DongchannN Mar 23, 2025
e82967f
refactor: 구체적인 예외 메시지 작성
DongchannN Mar 23, 2025
5790ae7
refactor: 디렉토리 분리
DongchannN Mar 23, 2025
f228648
refactor: Janggi 클래스 메서드명 변경 및 메서드간 책임 준수
DongchannN Mar 23, 2025
e263dc6
fix: 경로의 도착지 계산 방식 수정
DongchannN Mar 23, 2025
97231e7
refactor: Position 객체 생성 시 정적 팩토리 메서드 사용
DongchannN Mar 24, 2025
5eba43f
refactor: 컨벤션 통일
DongchannN Mar 24, 2025
98dbb2a
feat: 장기판 내에서 장기말을 이동하는 로직 구현
DongchannN Mar 24, 2025
f4ac3e9
refactor: 장기 출력 방식 사용자 친화적으로 수정
DongchannN Mar 24, 2025
a9385af
refactor: 불필요한 if문 indent 제거
DongchannN Mar 24, 2025
efbc7d8
refactor(README.md): 완료한 기능 요구사항 정리
DongchannN Mar 24, 2025
1752f6f
fix: stackOverflow 예외 해결
DongchannN Mar 25, 2025
b7ceb0f
refactor: 불필요한 개행 제거
DongchannN Mar 25, 2025
2468b88
refactor: 외부에서 사용하지 않는 메서드 private 으로 변경
DongchannN Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,71 @@
# java-janggi

장기 미션 저장소

## 기능 구현

### 장기(장기판), Janggi

- [x] 생성 시 장기판을 초기화한다.
- [x] 장기판의 위치를 선택하면, 해당 장기말이 움직일 수 있는 모든 경로를 반환한다.
- [x] 장기말을 장기판의 위치를 이용해 선택하고, 도착지를 지정해 해당 장기말을 움직일 수 있다.
- [x] 도착지가 규칙에 벗어나면 예외를 발생시킨다.
- [x] 도착지에 적군이 있다면 잡는다.

### 장기말(기물), Unit

- [x] 해당 기물이 위치로부터 갈 수 있는 모든 위치를 반환한다.
- [x] 반환하는 경로는 다른 기물의 위치를 고려하지 않는다.
- [x] 장기판 내의 좌표인지는 고려한다.

### 장기말 이동 규칙, UnitRule

- [x] 각 장기말의 이동 규칙을 알고있다.

### 장기판 내의 위치, Position

- [x] 장기판 내의 위치를 관리한다.
- [x] 범위를 벗어나서 위치를 생성하려하면 예외를 발생시킨다.

### 출력 형식

```
* | 00 01 02 03 04 05 06 07 08
--------------------------------
0 | CH EL HR GD .. GD EL HR CH
1 | .. .. .. .. GN .. .. .. ..
2 | .. CN .. .. .. .. .. CN ..
3 | SD .. SD .. SD .. SD .. SD
4 | .. .. .. .. .. .. .. .. ..
5 | .. .. .. .. .. .. .. .. ..
6 | SD .. SD .. SD .. SD .. SD
7 | .. CN .. .. .. .. .. CN ..
8 | .. .. .. .. GN .. .. .. ..
9 | CH EL HR GD .. GD EL HR CH

기물 별 대표 문자
궁(General) : GN
졸, 병(Soldiers) : SD
사(Guards) : GD
마(Horses) : HR
차(Chariots) : CH
포(Cannons) : CN
상(Elephants) : EL
```

## 1단계 블랙잭 피드백

- [x] 장기 네이밍 변경하기 [장기 기획서](https://en.wikipedia.org/wiki/Janggi)
- [x] `Team#isFront()` 메서드 삭제
- [x] `JanggiTest` 불필요한 줄바꿈 제거
- [x] `Position#isHorizontal()` -> `isHorizontalOrVertical()` 메서드 네임 변경
- [x] `ChariotUnitRule#calculateAllRoute()` 에서 `@Override` 애너테이션 붙이기
- [x] `CannonUnitRule#calculateEndPoint()` 접근제한자 `private`으로 변경
- [x] `OutputView`에서 if문에 도달하지 않는 경우 예외 발생시키기
- [x] `Janggi` 외부에서 주입하기
- [ ] 주입하는 위치 고민
- [x] `Janggi#changeTeam()`구현 로직 `Team` 객체에 메시지를 보내는 방향으로
- [x] `Unit` -> 동등성 정의 로직 삭제
- [x] `Unit` 객체 인스턴스 변수 3개를 2개로 줄이기
- [x] 에외 메시지 작성
- [ ] `DefaultUnitPosition`에서 기본 좌표 처리에 대한 고민
Empty file removed src/main/java/.gitkeep
Empty file.
20 changes: 20 additions & 0 deletions src/main/java/JanggiApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import domain.Game;
import view.InputView;
import view.OutputView;

public class JanggiApplication {

public static void main(String[] args) {
final InputView inputView = new InputView();
final OutputView outputView = new OutputView();
final Game game = new Game(inputView, outputView);

while (!game.isEnd()) {
try {
game.doTurn();
} catch (IllegalArgumentException e) {
System.out.println("[ERROR] " + e.getMessage());
}
}
}
}
69 changes: 69 additions & 0 deletions src/main/java/domain/Game.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package domain;

import domain.position.DefaultUnitPosition;
import domain.position.Position;
import domain.position.Route;
import domain.unit.Team;
import domain.unit.Unit;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import view.InputView;
import view.OutputView;

public class Game {

private final Janggi janggi;
private final InputView inputView;
private final OutputView outputView;

public Game(InputView inputView, OutputView outputView) {
this.janggi = createJanggi();
Copy link
Author

Choose a reason for hiding this comment

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

장기를 생성할 때 장기말들과 Turn을 정하는 것이 유동적이지 않다는 것에 동의하여 외부에서 의존성을 주입하는 것으로 변경하였습니다. 👍
지금은 Game에서 주입을 하지만 Game의 책임을 덜어주고, 나중에 상차림관련 로직을 위해 따로 클래스를 만드는 것을 고려하고 있습니다.
Game에서 주입을 하는 것이 자연스러운지, 따로 클래스를 생성하는 것이 자연스러운지 로키의 의견이 궁금합니다.

Copy link

Choose a reason for hiding this comment

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

Turn을 외부에서 주입하는 것이 조금 더 유연성이 높다고 생각해요. 😃
추후 어떤 팀이 선공을 할 것인지 입력을 받는다거나하는 변경이 생겼을 때도 유연하게 대처할 수 있을 것 같아요.
질문 주신 내용에 대해서도 생성자를 비즈니스 규칙에 맞춰 강하게 의존하도록 만들 필요는 없다고 생각해서 외부에서 주입받는 방식이 더 좋다고 생각해요.

다만 Game 객체의 역할이 조금 불분명하게 느껴집니다. 🤔
View에도 의존하고 도메인 객체에도 의존하다보니 일반적으로는 Controller의 역할을 할 것으로 보이는데, 도메인 객체를 또 상태로 가지고있다보니 Game 객체가 어떤 의도로 만들어진 것인지 조금 헷갈리는 것 같아요. 👀

Game객체는 View와의 의존성을 가지고있는 것 같은데, domain 패키지에 두신 것은 domain 모델을 의도하신걸까요?

Game 객체는 확실하게 (view 의존성 없이) 장기 게임을 진행하는 역할을 위임해보면 어떨까요?

this.inputView = inputView;
this.outputView = outputView;
}

private Janggi createJanggi() {
Map<Position, Unit> hanUnits = settingUnits(Team.HAN);
Map<Position, Unit> choUnits = settingUnits(Team.CHO);
return Janggi.of(hanUnits, choUnits, Team.CHO);
}

private Map<Position, Unit> settingUnits(Team team) {
Map<Position, Unit> units = new HashMap<>();
for (DefaultUnitPosition value : DefaultUnitPosition.values()) {
units.putAll(DefaultUnitPosition.createDefaultUnits(value, team));
}
return units;
}

public void doTurn() {
outputView.printJanggiUnits(janggi.getUnits());
Position pick = parsePosition(inputView.readUnitPosition(janggi.getTurn()));

List<Route> routes = janggi.findMovableRoutesFrom(pick);
outputView.printAvailableRoute(pick, routes);

Position destination = parsePosition(inputView.readDestinationPosition(janggi.getTurn()));

janggi.doTurn(pick, destination);
outputView.printJanggiUnits(janggi.getUnits());
}

public boolean isEnd() {
return janggi.isOneOfTeamNonExist();
}

private List<Integer> parseInteger(String rawPosition) {
return Arrays.stream(rawPosition.split(","))
.map(String::trim)
.map(Integer::parseInt)
.toList();
}

private Position parsePosition(String rawPosition) {
List<Integer> positionValue = parseInteger(rawPosition);
return Position.of(positionValue.get(0), positionValue.get(1));
}
}
164 changes: 164 additions & 0 deletions src/main/java/domain/Janggi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package domain;

import domain.position.Position;
import domain.position.Route;
import domain.unit.Team;
import domain.unit.Unit;
import domain.unit.UnitType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Janggi {

public static final String EMPTY_POINT_EXCEPTION = "해당 위치에 기물이 존재하지 않습니다.";
public static final String PICK_OPPOSITE_UNIT_EXCEPTION = "상대팀 말은 고를 수 없습니다.";
public static final String CANNOT_MOVE_EXCEPTION = "이동할 수 없는 도착지입니다.";

private final Map<Position, Unit> units;
private Team turn;

private Janggi(Map<Position, Unit> units, Team turn) {
this.units = new HashMap<>(units);
this.turn = turn;
}

public static Janggi of(Map<Position, Unit> hanUnits, Map<Position, Unit> choUnits, Team turn) {
Map<Position, Unit> units = new HashMap<>();
units.putAll(hanUnits);
units.putAll(choUnits);
return new Janggi(units, turn);
}

public void doTurn(Position pick, Position destination) {
if (!canMove(pick, destination)) {
throw new IllegalArgumentException(CANNOT_MOVE_EXCEPTION);
}
Unit pickedUnit = units.get(pick);
Unit destinationUnit = units.get(destination);
if (destinationUnit != null && destinationUnit.getTeam() == turn.getOpposite()) {
units.remove(destination);
}
units.remove(pick);
units.put(destination, pickedUnit);
Comment on lines +37 to +43
Copy link

Choose a reason for hiding this comment

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

질문 2. Janggi의 책임 분리에 대한 고민

지금 코드에서는 Janggi의 책임이 너무 막중하다고 생각합니다.
Unit에서 가능한 모든 경로를 가져온 다음 다른 기물들의 위치를 고려해 filter하는 로직이 책임을 가중시킨다고 생각합니다. 저는 Janggi 로직에 2가지 방법이 있다고 생각합니다.

필터링을 장기말들에서 하는 방법 (1)
장기말에서 필터링을 한다면 확실히 Janggi의 책임을 줄일 수 있을 것입니다.
하지만 Janggi가 Unit에서 가능한 루트를 반환받은 뒤, 필터링을 위해 다른 기물들을 파라미터로 주며 Unit에 요청하기에 흐름이 일관되지 않는 느낌입니다.

지금과 같은 방법 (2)
흐름이 자연스롭고 장기말이 다른 기물들의 위치를 몰라도 되기 때문에 캡슐화가 잘 지켜졌다고 생각합니다. 하지만 책임이 너무 막중하다는 단점이 있습니다.
저는 두 가지 방법 중에서 첫 번째 방법으로 경로 탐색 관련된 역할을 Unit에 집중시키는 것이 더 괜찮다고 판단하는데 로키의 의견이 궁금합니다.

말씀하신것처럼 1,2 둘 다 장단점이 확실히 보이는 것 같아요.
저는 3의 방법을 제안드려보고 싶은데요.

  1. Map<Position, Unit> units를 일급 컬렉션으로 랩핑해보면 어떨까요? 😃
    그렇다면 Janggi 객체의 막중한 책임이 조금은 해소되지 않을까 싶어요. 🤔

또한 findMovableRoutesFrom의 결과와 같이 List<Route> 또한 Routes와 같은 일급 컬렉션을 만들어서 Janggi 객체가 가지고 있는 역할을 일부 위임해줄 수도 있을 것 같아요.

지금처럼 Janngi 객체가 과한 책임이나 역할을 가지고있다면 다른 객체에게 적절히 위임해볼 수는 없을지 고민해보고
제가 제안드린 것과 같이 새로운 객체 혹은 이미 존재하지만 별다른 역할이 없는 객체 등을 훑어보며 적절히 역할을 위임하게된다면 차니가 고민하고 계신 부분이 어느정도 해결될 것 같아요.

switchTurn();
}

private boolean canMove(Position pick, Position destination) {
List<Route> movableRoutes = findMovableRoutesFrom(pick);
return movableRoutes.stream()
.map(route -> route.searchDestination(pick))
.anyMatch(position -> position.equals(destination));
}

private void switchTurn() {
turn = turn.getOpposite();
}

public List<Route> findMovableRoutesFrom(Position pick) {
if (isEmptyPosition(pick)) {
throw new IllegalArgumentException(EMPTY_POINT_EXCEPTION);
}
Unit pickedUnit = units.get(pick);
if (pickedUnit.getTeam() != turn) {
throw new IllegalArgumentException(PICK_OPPOSITE_UNIT_EXCEPTION);
}

List<Route> totalRoutes = pickedUnit.calculateRoutes(pick);
totalRoutes = filterRoutesByUnitType(pickedUnit, pick, totalRoutes);
if (pickedUnit.getType() == UnitType.CANNON) {
return totalRoutes;
}
return filterBlockedRoutes(pick, totalRoutes);
}

private List<Route> filterRoutesByUnitType(Unit pickedUnit, Position pick, List<Route> totalRoutes) {
UnitType type = pickedUnit.getType();
if (type == UnitType.CANNON) {
return totalRoutes.stream()
.filter(route -> canCannonJump(pick, route))
.toList();
}
if (type == UnitType.SOLDIER) {
return filterSoldierMoves(pick, pickedUnit, totalRoutes);
}
return totalRoutes;
}
Comment on lines +67 to +86
Copy link

Choose a reason for hiding this comment

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

한번 할당한 변수를 재할당할 필요 없이 지금처럼 filterRoutesByUnitType 메서드를 수행한 결과를 한번만 변수로 선언하면 될 것 같아요. 😉

(final은 꼭 안써도되지만, 해당 코멘트를 조금 더 강조하기위해 선언했어요 😃)

Suggested change
List<Route> totalRoutes = pickedUnit.calculateRoutes(pick);
totalRoutes = filterRoutesByUnitType(pickedUnit, pick, totalRoutes);
if (pickedUnit.getType() == UnitType.CANNON) {
return totalRoutes;
}
return filterBlockedRoutes(pick, totalRoutes);
}
private List<Route> filterRoutesByUnitType(Unit pickedUnit, Position pick, List<Route> totalRoutes) {
UnitType type = pickedUnit.getType();
if (type == UnitType.CANNON) {
return totalRoutes.stream()
.filter(route -> canCannonJump(pick, route))
.toList();
}
if (type == UnitType.SOLDIER) {
return filterSoldierMoves(pick, pickedUnit, totalRoutes);
}
return totalRoutes;
}
final List<Route> totalRoutes = filterRoutesByUnitType(pickedUnit, pick);
if (pickedUnit.getType() == UnitType.CANNON) {
return totalRoutes;
}
return filterBlockedRoutes(pick, totalRoutes);
}
private List<Route> filterRoutesByUnitType(Unit pickedUnit, Position pick) {
var routes = pickedUnit.calculateRoutes(pick);
UnitType type = pickedUnit.getType();
if (type == UnitType.CANNON) {
return routes.stream()
.filter(route -> canCannonJump(pick, route))
.toList();
}
if (type == UnitType.SOLDIER) {
return filterSoldierMoves(pick, pickedUnit, routes);
}
return routes;
}

추가적으로 filterRoutesByUnitType 지금처럼 바꾸게되면 행위가 조금 달라지니 네이밍도 바뀌는게 좋을 것 같네요 👀


private List<Route> filterSoldierMoves(Position pick, Unit pickedUnit, List<Route> totalRoutes) {
if (pickedUnit.getTeam() == Team.HAN) {
return totalRoutes.stream()
.filter(route -> route.getPositions().getFirst().getY() >= pick.getY())
.toList();
}
return totalRoutes.stream()
.filter(route -> route.getPositions().getFirst().getY() <= pick.getY())
.toList();
}

private boolean canCannonJump(Position current, Route route) {
Position endPoint = route.searchDestination(current);
Copy link

Choose a reason for hiding this comment

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

point와 position은 엄연히 다른 용도로 만드셨다고 했던 것 같은데요.
네이밍도 혼용되게 사용되지 않는게 좋을 것 같아요. 😃

if (!isEmptyPosition(endPoint)) {
Unit endUnit = units.get(endPoint);
if (endUnit.getType() == UnitType.CANNON) {
return false;
}
}

int count = 0;
for (Position position : route.getPositionsExceptDestination(current)) {
if (isEmptyPosition(position)) {
continue;
}
Unit unit = units.get(position);
if (unit.getType() == UnitType.CANNON) {
return false;
}
count++;
}
return (count == 1);
Copy link

Choose a reason for hiding this comment

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

1이 어떤 의미를 가지는건지 이해하기가 조금 어려운 것 같네요. 🤔
상수로 뽑고 네이밍을 통해서 의도를 드러내보면 어떨까요?

}
Comment on lines +108 to +120
Copy link

Choose a reason for hiding this comment

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

조금 중복이될 수도 있지만, 지금 코드는 한번에 여러 계산을하려다보니 조금 복잡하게 느껴지는 것 같아요.

아래와 같이 변경해보는건 어떻게 생각하시나요? 😃 (혹시 해당 피드백을 반영하신다면 의도를 드러낼 수 있는 더 좋은 네이밍을 고려해주시면 좋을 것 같습니다 😉)

Suggested change
int count = 0;
for (Position position : route.getPositionsExceptDestination(current)) {
if (isEmptyPosition(position)) {
continue;
}
Unit unit = units.get(position);
if (unit.getType() == UnitType.CANNON) {
return false;
}
count++;
}
return (count == 1);
}
var positions = route.getPositionsExceptDestination(current);
var hasCannon = positions.stream()
.filter(units::containsKey)
.map(units::get)
.anyMatch(unit -> unit.getType() == UnitType.CANNON);
return !hasCannon && onlyOne(positions);
}
private boolean onlyOne(List<Position> positions) {
var count = positions.stream()
.filter(units::containsKey)
.map(units::get)
.count();
return count == 1;
}


private List<Route> filterBlockedRoutes(Position pick, List<Route> routes) {
return routes.stream()
.filter(route -> isClearRoute(pick, route))
.filter(route -> isClearDestination(pick, route))
.toList();
}

private boolean isClearRoute(Position pick, Route route) {
return route.getPositionsExceptDestination(pick).stream()
.allMatch(this::isEmptyPosition);
}

private boolean isClearDestination(Position pick, Route route) {
Position endPosition = route.searchDestination(pick);
if (isEmptyPosition(endPosition)) {
return true;
}
Unit endPointUnit = units.get(endPosition);
return endPointUnit.getTeam() != this.turn;
}

private boolean isEmptyPosition(Position position) {
return !units.containsKey(position);
}

public boolean isOneOfTeamNonExist() {
long hanUnitCount = units.values().stream().
filter(unit -> unit.getTeam() == Team.HAN)
Copy link

Choose a reason for hiding this comment

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

unit 객체에게 메시지를 보내볼 수도 있을 것 같아요. 😃

.count();
long choUnitCount = units.values().stream().
filter(unit -> unit.getTeam() == Team.CHO)
.count();
return (hanUnitCount == 0 || choUnitCount == 0);
}

public Team getTurn() {
return turn;
}

public Map<Position, Unit> getUnits() {
return units;
}
}
51 changes: 51 additions & 0 deletions src/main/java/domain/position/DefaultUnitPosition.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package domain.position;

import domain.unit.Team;
import domain.unit.Unit;
import domain.unit.rule.CannonUnitRule;
import domain.unit.rule.ChariotUnitRule;
import domain.unit.rule.ElephantUnitRule;
import domain.unit.rule.HorseUnitRule;
import domain.unit.rule.NoneUnitRule;
import domain.unit.rule.SoldierUnitRule;
import domain.unit.rule.UnitRule;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public enum DefaultUnitPosition {

GENERAL(1, 8, List.of(4), NoneUnitRule::new),
GUARD(0, 9, List.of(3, 5), NoneUnitRule::new),
CHARIOT(0, 9, List.of(0, 8), ChariotUnitRule::new),
CANNON(2, 7, List.of(1, 7), CannonUnitRule::new),
SOLDIER(3, 6, List.of(0, 2, 4, 6, 8), SoldierUnitRule::new),
HORSE(0, 9, List.of(2, 7), HorseUnitRule::new),
ELEPHANT(0, 9, List.of(1, 6), ElephantUnitRule::new),
;

private final int hanY;
private final int choY;
Comment on lines +28 to +29
Copy link

Choose a reason for hiding this comment

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

남겨주신 코멘트에 대한 답변 여기서 이어서 진행할게요. 😃

왜 유동적인 면에서 차이가 나는 것일까요?
일단 Position으로 관리하면 반복되는 각 팀의 Y좌표가 더 관리하기 힘들다고 생각해서 로키의 피드백을 아직 반영하진 않았어요.

초기 좌표로 y가 꼭 하나가 아닌 경우에는 어떻게 되는걸까요?
지금은 y 좌표가 무조건 동일할거라는 가정이 깔려있다고 생각해요.

Copy link
Author

Choose a reason for hiding this comment

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

로키의 의견 이제 완전히 이해했어요! 감사합니다 👍
초기 y 좌표에 변경이 생기면 변경이 생길 수 있다는 것에는 저도 동의합니다. 확장성을 고려하는 것은 필수적이지만, 너무 미래에 대한 확장을 열어 놓는 것은 불필요하다고 생각해요.

관련해서 두 가지 의견이 있습니다. 들어보시고 로키의 의견 편하게 말씀해주세요! 😊

첫번째로는 장기의 초기 Y위치가 변경될 여지가 현저하게 적다는 점입니다. X좌표는 추후 상차림이라는 규칙으로 인하여 바뀔 가능성이 있지만 같은 유닛이 다른 Y좌표를 가질 수 있다는 규칙은 존재하지 않기 때문입니다.

두번째로는 사이드 이펙트가 거의 없다고 생각합니다. 추후에 Y좌표를 한 팀의 장기말에서 동일하지 않는다는 규칙이 생길 때 바뀌어도 늦지 않을 것이라고 생각해요.
만약 요구사항이 변경되어 로키가 제시해준 방법으로 바꾸었을 때 다른 클래스들이 영향을 받는다면 문제가 심해질 것이지만, 나중에 그런 상황이 발생해도 DefaultUnitPoisition이라는 클래스 하나만 수정해도 외부에 영향이 없기 때문이에요.

Copy link

Choose a reason for hiding this comment

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

초기 y 좌표에 변경이 생기면 변경이 생길 수 있다는 것에는 저도 동의합니다. 확장성을 고려하는 것은 필수적이지만, 너무 미래에 대한 확장을 열어 놓는 것은 불필요하다고 생각해요.

저도 DRY 원칙을 참 좋아하기 때문에 매우 공감하는 말이에요.

첫번째로는 장기의 초기 Y위치가 변경될 여지가 현저하게 적다는 점입니다. X좌표는 추후 상차림이라는 규칙으로 인하여 바뀔 가능성이 있지만 같은 유닛이 다른 Y좌표를 가질 수 있다는 규칙은 존재하지 않기 때문입니다.

먼저 첫번째 말씀은 저도 어느정도 동의합니다만, 너무 순수하게 전통적인 장기 게임으로만 생각해도 안될 문제라고 생각해요. 😃
온라인 게임으로 장기 게임을 서비스회사라면 꼭 전통적인 방식의 장기게임만 제공하라는 법은 없으니깐요. 😉

두번째로는 사이드 이펙트가 거의 없다고 생각합니다. 추후에 Y좌표를 한 팀의 장기말에서 동일하지 않는다는 규칙이 생길 때 바뀌어도 늦지 않을 것이라고 생각해요.
만약 요구사항이 변경되어 로키가 제시해준 방법으로 바꾸었을 때 다른 클래스들이 영향을 받는다면 문제가 심해질 것이지만, 나중에 그런 상황이 발생해도 DefaultUnitPoisition이라는 클래스 하나만 수정해도 외부에 영향이 없기 때문이에요.

지금은 규모가 작기 때문에 상대적으로 리팩터링이 쉬울 수 있지만, 실제로 이미 운영되고있는 서비스이고 또 규모도 커진다면...
생각한 것보다 변경이 쉽지 않을 수도 있다고 생각해요.
그래서 어느정도는 확장성을 고려하며 코드를 구현하는 것도 중요하다고 생각해요.

그래도 DefaultUnitPosition의 (getter를 써서 직접 처리하는 로직없이) 공개 메서드만 사용하고있는 상황이라면 추후 수정하는 것이 그리 어렵지 않을 것 같다는 말에는 동의합니다. 😃 (단지... 다른 팀원들이 공개 메서드만 쓴다는 보장은 없기 때문에... 이렇게 이상적으로 코드가 유지보수될지는 확신하기는 어려울 것 같네요)

그리고 무엇보다도...
저희는 미션 요구사항에서 3개 이상의 인스턴스 변수를 가진 클래스를 만들지 않는다.라는 요구사항을 만족해야하기도하죠. 😅
저는 인스턴스 변수를 줄이는 방면을 고민하다보니 각팀의 y좌표와 x좌표들을 따로 관리하는 것보다는 묶음으로 관리하는게 낫지 않나 싶었어요. 😃

차니의 의견도 충분히 납득이되기 때문에 제가 제안드린 방식이 꼭 아니더라도 상관없으니 미션 요구사항에 맞게 인스턴스 변수 갯수를 줄여보면 좋을 것 같아요. 😃

private final List<Integer> xPositions;
private final Supplier<UnitRule> rule;

DefaultUnitPosition(int hanY, int choY, List<Integer> xPositions, Supplier<UnitRule> rule) {
this.hanY = hanY;
this.choY = choY;
this.xPositions = xPositions;
this.rule = rule;
}

public static Map<Position, Unit> createDefaultUnits(DefaultUnitPosition position, Team team) {
if (team == Team.CHO) {
return position.xPositions.stream()
.map(x -> Position.of(x, position.choY))
.collect(Collectors.toMap(pos -> pos, pos -> Unit.of(team, position.rule.get())
));
}
return position.xPositions.stream()
.map(x -> Position.of(x, position.hanY))
.collect(Collectors.toMap(pos -> pos, pos -> Unit.of(team, position.rule.get())));
}
}
Loading