[JAVA] 사다리타기

함형주·2024년 1월 5일
0

KUIT

목록 보기
1/2

질문, 피드백 등 모든 댓글 환영합니다.

개요

건국대학교 교내 IT 동아리 KUIT에서 진행한 1~2주차 미션에 관한 내용입니다.
Back-end 학습에 앞서 자바의 핵심 요소인 객체지향에 익숙해지기 위해 소트웍스 엔솔로지의 객체지향 생활체조 원칙을 준수하여 아래 요구사항에 맞춰 개발합니다.

요구사항

  1. row, col 값을 인자로 Ladder 클래스를 생성
  2. LadderGame.run(int) 메서드를 통해 시작 사다리 번호를 입력하면 결과 사다리 번호를 반환
  3. 사다리 출력
  • Before
    1* -1 0 0 0 
    0 1 -1 0 0 
    0 0 0 0 0 
    0 0 0 0 0 
    // 사다리의 1을 만났으니 왼쪽으로 이동한다.
    After
    1 -1* 0 0 0 
    0 1 -1 0 0 
    0 0 0 0 0 
    0 0 0 0 0 
    // 이동 후 아래로 한칸 내려간다.
    Before
    1 -1 0 0 0 
    0 1* -1 0 0 
    0 0 0 0 0 
    0 0 0 0 0 
    // 반복 ...
  1. 사다리 자동 생성
  • 정책에 맞춰서 사다리 가로대(Rung)을 랜덤하게 생성
  • Rung 개수는 사다리 행*열 *0.3
  • 사다리의 Rung 은 겹쳐져서 생성될 수 없다. (e.g., 1 -1 -1 / 1 1 -1)
  • 가능하다면 LadderGame에 인터페이스를 활용한 의존성 주입을 통해 LadderCreator를 변경할 수 있도록 해본다.
  • 정적 팩토리 메서드 패턴을 활용하여 LadderGame을 생성해보자.
    • e.g., LadderGame ladderGame = LadderGameFactory.createRandomLadderGame()
    • 의존성 주입을 팩토리 메서드가 맡을 수 있게 된다.
    • 즉, LadderGame을 생성하는 책임을 한 클래스가 맡게 된다!

  • 객체 지향적으로 설계 및 개발
    • 메서드에 하나의 작업만 부여
  • 단위 테스트를 작성

전체 코드 : GitHub

개발

LadderGame

사다리 게임의 시작 부분으로 사다리 번호를 입력하면 사다리게임을 진행하여 최종 사다리 번호를 반환.

    private final LadderCreator ladderCreator;

    public static LadderGame of(LadderCreator ladderCreator) {
        return new LadderGame(ladderCreator);
    }

    public int run(LadderNumber ladderNum) {
        CurrentPosition currentPosition = CurrentPosition.createCurrentPosition(ladderNum);
        LadderRunner.of(ladderCreator.getLadder()).run(currentPosition);
        return currentPosition.getY();
    }

LadderGameFactory

LadderGame 생성과 LadderCreator에 대한 의존성 주입 담당.

public class LadderGameFactory {

    private LadderGameFactory() {}

    public static LadderGame createSelfLadderGame(int row, int numberOfPerson) {
        SelfLadderCreator selfLadderCreator = new SelfLadderCreator(
                        Ladder.of(NumberOfRow.of(row), NumberOfPerson.of(numberOfPerson)));
        return LadderGame.of(selfLadderCreator);
    }

    public static LadderGame createRandomLadderGame(int row, int numberOfPerson) {
        Ladder ladder = Ladder.of(NumberOfRow.of(row), NumberOfPerson.of(numberOfPerson));
        SelfLadderCreator selfLadderCreator = new SelfLadderCreator(ladder);
        RandomLadderCreator randomLadderCreator = new RandomLadderCreator(ladder, selfLadderCreator);
        return LadderGame.of(randomLadderCreator);
    }

}

LadderCreator

사다리의 가로 선을 생성하는 역할.

public interface LadderCreator {
    void drawLine(int x, int leftY, int rightY);
    void drawLine();
    Ladder getLadder();
}
  • LadderCreator는 두가지 방식을 사용한다.
    1. 지정한 위치에 사다리 생성
    2. 랜덤한 위치에 사다리 생성
  • 때문에 두 기능을 구현하기 위해 인터페이스로 정의하여 사용한다.
public class SelfLadderCreator implements LadderCreator{

    private final Ladder ladder;

    public SelfLadderCreator(Ladder ladder) {
        this.ladder = ladder;
    }

    @Override
    public void drawLine(Rung rung) {
        ladder.setLine(rung.getLeftPointXInt(),
                rung.getLeftPointYInt(), rung.getRightPointYInt());
    }

    /**
     * @deprecated
     */
    @Override
    public void drawLine() {
        throw new UnsupportedOperationException();
    }
    ...
  • 랜덤한 위치의 지정과 사다리 생성 기능을 분리하기 위해 조합을 사용하였다.
public class RandomLadderCreator implements LadderCreator {

    private final Ladder ladder;
    private final LadderCreator selfLadderCreator;

    public RandomLadderCreator(Ladder ladder, LadderCreator selfLadderCreator) {
        this.ladder = ladder;
        this.selfLadderCreator = selfLadderCreator;
        drawLine();
    }

    /**
     * @deprecated
     */
    @Override
    public void drawLine(int x, int leftY, int rightY) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void drawLine() {
        int count = (int) (ladder.getRowSize() * ladder.getNumberOfPersonSize() * 0.3);

        Set<Rung> randomRungs = RandomRung.getRandomRungs(ladder, count);

        randomRungs.forEach(rung -> selfLadderCreator.drawLine(rung));
    }
	...

RandomRung

사다리 생성 정책에 맞도록 랜덤한 위치를 가지는 Rung을 개수에 맞게 생성하여 반환.

  • 사다리의 가로 라인은 연속으로 존재할 수 없기에 Rung의 가로 세로 좌표를 기준으로 equals(), hash()를 override 하여 Set에 담길 때 중복되어 생성될 수 없도록 하였다.
    public static Set<Rung> getRandomRungs(Ladder ladder, int count) {
        Set<Rung> randomRungSet = new HashSet<>(count);

        while (randomRungSet.size() != count){
            randomRungSet.add(getRandomRung(ladder));
        }

        return randomRungSet;
    }

    private static Rung getRandomRung(Ladder ladder) {
        int randomX = RandomUtil.generate(ladder.getRowSize());
        int randomY = RandomUtil.generate(ladder.getNumberOfPersonSize() - 1);

        return Rung.of(randomX, randomY, randomY + 1);
    }

Rung

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Rung rung = (Rung) o;
        return Objects.equals(leftPoint, rung.leftPoint) || Objects.equals(rightPoint, rung.rightPoint)
                || Objects.equals(rightPoint, rung.leftPoint) || Objects.equals(leftPoint, rung.rightPoint);
    }

    @Override
    public int hashCode() {
        return 1;
    }

LadderRunner

사다리 게임의 본격적인 시작이 진행되는 클래스.

  • 사다리 게임 진행의 현황을 출력하는 예제 조건을 만족하기 위해 LadderViewer 클래스를 정의하였다.
  • CurrentPosition을 기반으로 사다리게임의 진행과 출력을 구현한다.
    private final Ladder ladder;

    public void run(CurrentPosition currentPosition) {
        LadderViewer ladderViewer = LadderViewer.of(ladder);

        for (int i = 0; i < ladder.getRowSize(); i++) {
            currentPosition.setX(i);
            ladderViewer.view("BEFORE", currentPosition);
            ladder.nextPosition(i, currentPosition);
            ladderViewer.view("AFTER", currentPosition);
        }
    }

LadderViewer

사다리게임의 현황을 출력.

  • 사다리 게임의 진행 전후로 현황에 대한 출력을 보장해야 하므로 LadderViewer에서 실질적인 사다리의 진행을 제어한다.
public class LadderViewer {

    private final CurrentPosition currentPosition;
    private final Ladder ladder;

    public void view(String message, CurrentPosition currentPosition) {
        System.out.println(message);
        ladder.view(currentPosition);
        System.out.println();
    }
}

Ladder

2차원 배열을 기반으로 사다리를 구성.

  • 2차원 배열을 한 클래스에서 모두 다루면 책임이 커지므로 2차원 배열의 행과 열을 Row[ ], Row.Node[ ] 로 분리하여 구현한다.
  • 각 행과 열에 관한 로직은 Row, Node에 위임하여 단일 책임 원칙을 준수하도록 했다.
    private final Row[] rows;

    private Ladder(NumberOfRow row, NumberOfPerson numberOfPerson) {
        rows = new Row[row.getNumberOfRow()];
        for (int i = 0; i < row.getNumberOfRow(); i++) {
            rows[i] = new Row(numberOfPerson);
        }
    }

    public static Ladder of(NumberOfRow row, NumberOfPerson numberOfPerson) {
        return new Ladder(row, numberOfPerson);
    }

    public void nextPosition(int i, CurrentPosition currentPosition) {
        rows[i].nextPosition(currentPosition);
    }

    public void view(CurrentPosition currentPosition) {
        for (int i = 0; i < getRowSize(); i++) {
            rows[i].viewValues(i, currentPosition);
        }
    }

    public void setLine(int x, int leftY, int rightY) {
        rows[x].setValue(leftY, rightY);
    }

Row

사다리 도메인에서 하나의 행을 관리.

  • node의 값을 비교하여 CurrentPosition(현재 위치) 값을 변화시킨다.
public class Row {

    private Node[] nodes;

    public Row(NumberOfPerson numberOfPerson) {
        this.nodes = new Node[numberOfPerson.getNumberOfPerson()];
        for (int i = 0; i < numberOfPerson.getNumberOfPerson(); i++) {
            nodes[i] = Node.createCenterNode();
        }
    }

    public void nextPosition(CurrentPosition currentPosition) {
        validateCurrentPositionY(currentPosition);

        if (nodes[currentPosition.getY()].isLeft()) {
            currentPosition.goRight();
            return;
        }
        if (nodes[currentPosition.getY()].isRight()) {
            currentPosition.goLeft();
            return;
        }
    }
	...
  • 출력도 마찬가지로 출력하는 노드의 위치와 CurrentPosition 의 값이 같을 경우 * 표시를 추가한다.
    public void viewValues(int row, CurrentPosition currentPosition) {
        for (int i = 0; i < nodes.length; i++) {
            if (currentPosition.equal(row, i)) {
                System.out.print(getValue(i) + "* ");
                continue;
            }
            System.out.print(getValue(i) + "  ");
        }
        System.out.println();
    }

Node

public class Node {

    private Direction direction;

    public int getValue() {
        return direction.getDirection();
    }

    public boolean isLeft() {
        return direction.equals(Direction.LEFT);
    }

    public boolean isRight() {
        return direction.equals(Direction.RIGHT);
    }
}

의문

LadderCreator

해당 예제에서는 정적 팩토리 패턴을 통한 DI를 설명하고 구현하기 위해서 LadderCreator를 인터페이스로 정의 후 LadderGameFactory를 통해 의존성을 주입해주는 방식의 요구사항이 있었습니다.
DI를 예제에 녹여내기 위해 이런 요구사항이 주어진 것은 이해하지만 한가지 의구심이 남습니다.

  • 인터페이스의 사용 목적은 추상화를 통한 DI 구현도 있지만, 다형성의 극대화를 위한 OCP 준수를 위함도 있습니다. 즉 팩토리 패턴을 통해 SelfLadderCreator/RandomLadderCreator 둘 중 어느 것을 주입하여도 정상적으로 동작해야 하는데 해당 예제는 그렇지 못합니다. 이는 LadderCreator와 그 구현체가 is-a 관계가 아님에도 상속을 통해 강한 결합을 가져 발생하는 문제라고 생각됩니다. 때문에 인터페이스로 정의하기보단, 조합(Composition)을 통해 두 클래스를 다루는 것이 적합하다고 생각됩니다.
  • 만약 인터페이스를 통한 DI를 적용하며 동시에 SOILD 원칙을 준수하기 위한 예제를 구성해야 한다면 RandomLadderCreator에서 '랜덤하게 생성되는 line'에 대한 정책에 대한 부분을 인터페이스로 정의하고, 이를 주입받아 사용하는 방식으로 설계한다면 어떨까 생각합니다.(일반적으로 사다리게임은 모든 사다리가 연결되어 있어 1번 사다리에서 마지막 사다리로 이동이 가능하도록 되어있습니다. 무분별한 랜덤 정책에서 앞서 말한 부분을 포함하도록 정책을 새로 정의하여 사용할 수 있을 것 같습니다.)
profile
평범한 대학생의 공부 일기?

0개의 댓글