객체 지향적인 코드를 구성하는 방법에는 상속 합성 조합 이 있습니다.
우아한 테크코스에서 장기 를 하며, 해당 개념에 대해 학습한 후 어떻게 사용하였는 지 정리하였습니다.
1. 상속
상속 은 객체의 책임, 역할은 같으나 구현 방식이 다를 때 사용되어야 합니다.
장기에는 차, 마, 상, 사, 궁, 포, 졸(병) , 총 7가지 기물이 있습니다.
미션에서 제가 정의한 기물(Piece) 의 역할은 보드 위의 특정 좌표까지 갈 수 있다면 경로를 제공하는 것이었습니다. 이동 가능성을 확인하고 경로를 제공하는 세부적인 구현이 7개 이상 나오겠다는 생각을 하여 상속 을 사용하였습니다.
추가적으로 기물(Piece) 의 경우에는 각자 다른 팀과 타입을 이동 가능성을 확인하기 위해 가져야하기 때문에 추상 클래스로 구현하였습니다.
moveStrategy 에 대해서는 후술하겠습니다.
public abstract class Piece {
protected final MoveStrategy moveStrategy;
private final PieceType pieceType;
private final Team team;
public Piece(MoveStrategy moveStrategy, PieceType pieceType, Team team) {
this.moveStrategy = moveStrategy;
this.pieceType = pieceType;
this.team = team;
}
abstract public void check(BoardStatus boardStatus, Position start, Position destination);
abstract public boolean isEmpty();
}
이후 포 를 제외한 모든 기물들의 이동 경로를 확인하는 과정에서 다른 기물을 뛰어 넘을 수 없다는 공통점을 공통화하기 위해서 NonJumpable 이라는 추상 클래스를 공통으로 도입하였습니다.
public abstract class NonJumpable extends Piece {
public NonJumpable(MoveStrategy moveStrategy, PieceType pieceType, Team team) {
super(moveStrategy, pieceType, team);
}
@Override
public void check(BoardStatus boardStatus, Position start, Position destination) {
List<Position> movablePath = moveStrategy.findMovablePath(start, destination);
checkPathIsEmpty(boardStatus, movablePath);
}
@Override
public boolean isEmpty() {
return false;
}
private void checkPathIsEmpty(BoardStatus boardStatus, List<Position> movablePaths) {
for (Position movablePath : movablePaths) {
if (isOccupied(boardStatus, movablePath)) {
throw new IllegalArgumentException(PieceErrorMessage.PATH_BLOCKED.getMessage());
}
}
}
private boolean isOccupied(BoardStatus boardStatus, Position movablePath) {
return boardStatus.status().containsKey(movablePath);
}
}
2. 조합과 합성
조합(Aggregation) 과 합성(Composition) 의 경우 둘 다 Has-a 관계이지만 둘 사이에도 차이점이 있었습니다.
정리하자면 다음과 같습니다.
| 구분 | 합성 (Composition) | 조합 (Aggregation) |
|---|---|---|
| 관계 강도 | 강한 결합 (Strong Has-a) | 약한 결합 (Weak Has-a) |
| 생명주기 | 공유함. 전체가 소멸하면 부분도 소멸. | 독립적임. 전체가 소멸해도 부분은 남음. |
| 소유권 | 독점적 소유. | 공유 가능 (여러 곳에서 참조 가능). |
| 구현 방식 | 내부에서 직접 생성 (new). |
외부에서 주입받음 (DI). |
다른 사람이 작성한 코드에서 조합과 합성을 구분하는 방법은 DI 와 new 키워드였지만, 직접 작성하는 경우에는 생명 주기 를 고민하는 것이 가장 효과적이었습니다.
조합
조합을 사용한 부분들 중 하나는 앞에서 후술하겠다고 한 moveStrategy 였습니다.
Piece 를 설계하는 과정에서 몇 몇 제약 조건이 없이 보드 위에 특정 한 기물만 있다는 조건이면 움직임이 동일한 기물이 있음을 확인했습니다. 예를 들어 포 와 차 의 경우, 다른 기물과의 관계를 생각하지 않으면 상하좌우로 이동 칸 수의 제약 없이 얼마든지 이동할 수 있다는 공통점이 있었습니다.
다른 기물에서도 다른 기물이 없는 경우에는 움직임이 비슷한 경우가 존재하여 다른 기물과의 관계를 고려하지 않는 경우 목적지까지 이동할 수 있는 이동 경로를 제공한다 는 책임을 가지는 strategy 를 도입하였습니다.
예시
*PalaceMoveRule 도 동일한 경우라 추가적인 설명에서 제외.*
- 공통 인터페이스 정의
package domain.piece.strategy;
import domain.position.Position;
import java.util.List;
public interface MoveStrategy {
List<Position> findMovablePath(Position start, Position destination);
}
- 상하좌우 이동 거리 제약이 없는
strategy구현
public class SlidingMoveStrategy implements MoveStrategy {
private final PalaceMoveRule palaceMoveRule;
public SlidingMoveStrategy(PalaceMoveRule palaceMoveRule) {
this.palaceMoveRule = palaceMoveRule;
}
@Override
public List<Position> findMovablePath(Position start, Position destination) {
if (start.isSameRow(destination.getRow())) {
return start.getSameRowPositionsToDestination(destination);
}
if (start.isSameColumn(destination.getColumn())) {
return start.getSameColumnPositionsToDestination(destination);
}
if (palaceMoveRule.isPalacePath(start, destination)) {
return List.of();
}
throw new IllegalArgumentException(MoveStrategyErrorMessage.NOT_EXIST_MOVABLE_PATH.getMessage());
}
}
- 사용
new Po(new SlidingMoveStrategy(palaceMoveRule);
new Cha(new SlidingMoveStrategy(palaceMoveRule);
합성
합성을 사용한 곳은 BoardInitializer 입니다.
이 객체는 보드가 관리하는 상태의 형태인 Map<Position, Piece> 타입에 맞게 모든 기물을 넣어서 제공하는 역할을 가지고 있습니다.
이 객체를 합성으로 사용한 이유는 우선 해당 객체는 Board 가 없다면, 의미가 없는 쓰레기 코드가 되기 때문이었습니다. 조합을 사용하지 않은 이유는 해당 객체의 경우에는 Board 의 초기값 생성이라는 Board 와 강력하게 결합된 책임이어서 공통으로 사용하지 않기 때문입니다.
설계를 하고 나서 해당 글을 작성하면서 보니 만약 조합 을 사용한다면 getter 의 사용이 필수가 됩니다. 이 경우, BoardInitializer 의 역할은 보드가 관리하는 상태의 타입에 따라 기물과 위치 초기값들을 구성한다가 되고, 자율성을 잃게 되어 변경에 취약할 것 같아, 객체 지향의 사용 이점을 잃게 되기도 합니다.
BoardInitializer
public class BoardInitializer {
private final Map<PieceType, Piece> choPieces;
private final Map<PieceType, Piece> hanPieces;
public BoardInitializer() {
choPieces = initPieceByTeam(Team.CHO);
hanPieces = initPieceByTeam(Team.HAN);
}
private Map<PieceType, Piece> initPieceByTeam(Team team) {
PalaceMoveRule palaceMoveRule = new PalaceMoveRule();
Map<PieceType, Piece> pieces = new HashMap<>();
pieces.put(PieceType.PO, new Po(new SlidingMoveStrategy(palaceMoveRule), team));
pieces.put(PieceType.MA, new Ma(new MaMoveStrategy(), team));
pieces.put(PieceType.SANG, new Sang(new SangMoveStrategy(), team));
pieces.put(PieceType.SA, new Sa(new SingleStepMoveStrategy(palaceMoveRule), team));
pieces.put(PieceType.JANG, new Jang(new SingleStepMoveStrategy(palaceMoveRule), team));
pieces.put(PieceType.CHA, new Cha(new SlidingMoveStrategy(palaceMoveRule), team));
pieces.put(PieceType.JOL, new Jol(new JolMoveStrategy(palaceMoveRule), team));
pieces.put(PieceType.BYEONG, new Byeong(new ByeongMoveStrategy(palaceMoveRule), team));
return pieces;
}
public Map<Position, Piece> setup(SettingType choSettingType, SettingType hanSettingType) {
Map<Position, Piece> defaultSetting = initPiecesWithoutSangAndMa();
initSangAndMa(choSettingType, hanSettingType, defaultSetting);
return defaultSetting;
}
{아래 생략}
- 사용
public class Board {
private final Map<Position, Piece> pieces;
private Board(Map<Position, Piece> pieces) {
this.pieces = pieces;
}
public static Board of(SettingType choSettingType, SettingType hanSettingType) {
Map<Position, Piece> setup = new BoardInitializer().setup(choSettingType, hanSettingType);
return new Board(setup);
}
{생략}
}
반박 너무 환영합니다.
반박해주시면 정말 감사하겠습니다.