티스토리 뷰

상태패턴

기능이 상태에 따라 다르게 동작해야할 때 사용하는 패턴이다. 상태별로 처리코드를 분리함으로써 컨텍스트의 코드가 간결해지고 새로운 상태가 추가되더라도 콘텍스트가 받는 영향이 최소화 된다.

  • 상태에 따라 동일한 기능 요청의 처리를 다르게 함
  • 콘텍스트(사용하는 문맥)이 상태를 가지고 있다.
  • 상태 인터페이스를 구현한 상태 콘크리트 클래스를 생성한다.

상태변경은 누가하는가?

상태를 변경하는 주체는 콘텍스트나 상태 객체 둘 중 하나다.

  • 컨텍스트가 상태를 변경하는 경우
    • 상태객체는 자신이 할 작업만 처리한다.
    • 비교적 상태 개수가 적고 상태 변경 규칙이 거의 바뀌지 않는 경우 유리하다.
  • 상태 객체에서 상태를 변경하는 경우
    • 컨텍스트에 영향을 주지 않으면서 상태를 추가하거나 상태 변경 규칙을 바꿀 수 있다.

블랙잭 미션에 적용

✨1. 상태 패턴을 만들기

 

Running과 Finished의 가장 차이점이 처음에는 takeCard라고 생각했다.(생각해보면 Blackjack인 경우에도 Player가 똑똑하지 않다면 카드를 더 받을 수 있다.;;) 하지만 미션을 적용하면서 느낀 가장 큰 차이점은 calculateScore 부분이었다. Ace가 중간계산용으로 1점으로 계산되는지, 최종계산용으로 11점으로 계산되는지 여부가 가장 크게 느껴졌다.

 

public interface State {
    State takeCard(Card card);

    boolean isBlackjack();

    boolean isBurst();

    Score calculateScore();

    Cards getCards();

    int size();

}

먼저 Interface에 위와 같이 공통되는 메서드를 만든다. Running에서는 takeCard만 다르게 동작할 것이다. 따라서 아래와 같이 abstract class Running을 만들어 준다. 카드의 점수에 따라 상태가 바뀌고 해당 카드로 점수계산 로직이 들어가게 된다. 따라서 Card를 가지고 다녀야 하기 때문에 생성자에서 Cards를 인자로 받아 속성으로 가지고 있어야 한다. 

public abstract class Running implements State {
    protected final Cards cards;

    public Running(Cards cards) {
        this.cards = cards;
    }
	
    @Override
    public boolean isBlackjack() {
        return false;
    }

    @Override
    public boolean isBurst() {
        return false;
    }

    @Override
    public Score calculateScore() {
        return cards.sumCards();
    }

    @Override
    public Cards getCards() {
        return cards;
    }

    @Override
    public int size() {
        return cards.size();
    }

}

달라질 수 있는 부분인 takeCard는 구체클래스 Hit에서 구현해준다. 이때, 21을 기준을 new Burst, new Blackjack, new Hit를 다시 리턴한다. 

public class Hit extends Running {
    public Hit(Cards cards) {
        super(cards);
    }

    @Override
    public State takeCard(Card card) {
        cards.takeCard(card);
        final Score score = calculateScore();

        if (score.isBurst()) {
            return new Burst(cards); // 점수계산이 21을 넘으면 burst를 리턴한다.
        }
        if (score.isBlackjack()) {
            return new Blackjack(cards); // 21인 경우 blackjack을 리턴한다.
        }

        return new Hit(cards); // 그 외는 실행 가능 상태이다.
    }
    
}

 

그럼 이번엔 Finished를 살펴보자. Running과 구분되는 가장 큰 점은 게임이 끝난다는 것이고, 이 경우 Ace의 계산로직이 다르게 적용된다. 

public abstract class Finished implements State {
    protected Cards cards;

    public Finished(Cards cards) {
        this.cards = cards;
    }

    @Override
    public Score calculateScore() {
        return cards.sumCardsForResult(); // Running과 가장 다른점은 Ace가 다르게 계산되는 것
    }

    @Override
    public Cards getCards() {
        return cards;
    }

    @Override
    public int size() {
        return cards.size();
    }
    
    // takeCard 경우는 기본적으로 Exception을 던지고 
    //Blackjack의 경우에만 오버라이드 하여 카드를 받고 Burst를 리턴하게 할 수 있다.
}

그 외 구체클래스인 Blackjack, Burst, Stay에서 각각 takeCard, isBlackjack, isBurst를 구현해주면 된다.

 

🏃‍♂️2. 상태 패턴을 Player에 적용

이것을 어떻게 적용할 수 있을까?? 바로 Player, Dealer 클래스 (Participant 클래스)에서 속성으로 갖고 있게 하면 된다. Player와 Dealer가 상속하는 Participant 클래스로 묶어줄 수 있다. 따라서 상태에 따른 다른 메서드의 적용이 가능해진다. 여기서 상태가 Cards를 가지고 있기 때문에 (카드와 관련된 점수계산 로직 등도) 상태에 의존하여 다르게 적용이 가능해진다.

public abstract class Participant implements Playable {
    private final Name name;
    private final BettingMoney bettingMoney;
    private State state;

    public Participant(String name, Cards cards, BettingMoney bettingMoney) {
        this.name = new Name(name);
        this.bettingMoney = bettingMoney;
        this.state = new Hit(cards);
    }

    @Override
    public String getName() {
        return this.name.toString();
    }

    @Override
    public void takeCard(Card card) {
        state = state.takeCard(card);
    }

    @Override
    public boolean isBlackjack() {
        return state.isBlackjack();
    }

    @Override
    public boolean isBurst() {
        return state.isBurst();
    }

    @Override
    public Score finalScore() {
        return state.calculateScore();
    }

    @Override
    public Cards getCards() {
        return state.getCards();
    }

    @Override
    public int sizeOfCards() {
        return state.size();
    }

    @Override
    public void stay() {
        this.state = new Stay(state.getCards());
    }

    public BettingMoney getBettingMoney() {
        return bettingMoney;
    }

    public State getState() {
        return state;
    }

}

😅 3. 좀 더 어려웠던 점

여기서 힘들었던 것은 stay() 부분이다. 다른 상태는 초기화 당시 Hit 상태로 세팅해주는 것 이외에는 상태에서 다른 상태로 객체를 반환해서 만들어준다. 하지만, stay의 경우에는 Dealer는 16초과시, Player는 `n`을 입력시(View와 관련) 변하게 되므로 상태 내부에서 생성이 불가했다. 따라서 내 머릿속에서는 stay()라는 상태를 변경해주는 메소드를 만들 수 밖에 없었다😭

 

Dealer의 경우에는 BlackjackGame 클래스에서 더이상 카드 받기가 불가할 때, stay()를 호출하여 변경해 주었다.

Player의 경우에는 Controller 역할을 하는 Application 클래스에서 View의 답변에 따라 stay()를 호출해 주었다.

 

결국, 1.컨텍스트에서 상태를 변경하는 경우와 2.상태에서 상태를 변경하는 경우, 두 가지 경우를 모두 사용하게 되었다.


파사드 패턴

코드 중복과 직접적인 의존을 해결하는데 도움을 주는 패턴이다. 파사드 패턴은 서브 시스템을 감춰주는 상위 수준의 인터페이스를 제공하여 문제를 해결한다. 구체적인 클라이언트와 시스템 구현 사이에 파사드를 두어 매개자 역할을 한다. 클라이언트가 파사드를 통해서 서브 시스템에 간접적으로 접근한다.

  • 라이브러리를 사용하는 코드를 읽기 쉽게 해준다.
  • 라이브러리를 사용하는 바깥쪽의 코드가 라이브러리 내부의 코드에 의존하는 것을 감소시킨다.
  • 쉽고 단순한 인터페이스를 사용하고 싶을 때 사용한다. 좋게 작성되지 않은 API를 사용하기 편한 API로 추상화하여 사용하기 편리하게 해줄 수 있다.

정리

상태패턴이 너무 어려워서 수업을 듣고 나서도, 공부하고 적용하는데 한참이 걸렸다. 상태에 따라 다른 행동을 해줄 수 있다는 것이 가장 큰 장점이라고 생각한다. 상태에 따라 분기함으로써, 전체적인 로직을 깔끔하게 유지할 수 있다. Step2가 merge 되고 나서도 걸리는 부분이 있어 몇군데 더 리팩토링을 진행하며 상태패턴을 좀 더 깔끔하게 적용해보려고 했다.

 

최종 리팩토링 한 내 브랜치

참고

  • [책] 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴
  • 우아한 테크코스 수업
  • 파사드 패턴 위키
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함