OOP

객체 지향 : 상속, 합성, 조합

by rati

객체 지향적인 코드를 구성하는 방법에는 상속 합성 조합 이 있습니다.

우아한 테크코스에서 장기 를 하며, 해당 개념에 대해 학습한 후 어떻게 사용하였는 지 정리하였습니다.

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);
    }

    {생략}

}

 

 

 

반박 너무 환영합니다.

반박해주시면 정말 감사하겠습니다.