State Pattern
영업 사원 카르멧 깃(Karmen Git)은 시장을 조사한 후, 사용자별 맞춤 기능을 제공하기로 결정했다
페드로: 요구 사항이 어렵지는 않네요
이브: 요구 사항을 명확하게 해 보죠
- 유료 사용자이면 모든 뉴스를 보여준다.
- 그렇지 않으면, 최근 10개의 뉴스만을 보여준다
- 돈을 지불하면 그 금액을 그의 계정 잔액에 더한다
- 무료 사용자의 잔액이 충분하면 상태를 유료 사용자로 변경한다.
페드로: 상태 패턴이네요! 멋진 패턴이죠. 먼저 사용자의 상태를 나타내는 enum을 만듭니다.
public enum UserState {
SUBSCRIPTION(Integer.MAX_VALUE),
NO_SUBSCRIPTION(10);
private int newsLimit;
UserState(int newsLimit) {
this.newsLimit = newsLimit;
}
public int getNewsLimit() {
return newsLimit;
}
}
페드로: User의 로직은 다음과 같습니다.
public class User {
private int money = 0;
private UserState state = UserState.NO_SUBSCRIPTION;
private final static int SUBSCRIPTION_COST = 30;
public List<News> newsFeed() {
return DB.getNews(state.getNewsLimit());
}
public void pay(int money) {
this.money += money;
if (state == UserState.NO_SUBSCRIPTION
&& this.money >= SUBSCRIPTION_COST) {
// buy subscription
state = UserState.SUBSCRIPTION;
this.money -= SUBSCRIPTION_COST;
}
}
}
페드로: 호출해 보죠
User user = new User(); // create default user user.newsFeed(); // show him top 10 news user.pay(10); // balance changed, not enough for subs user.newsFeed(); // still top 10 user.pay(25); // balance enough to apply subscription user.newsFeed(); // show him all news
이브: 행위(behavior)에 영향을 주는 값을 User 객체 안에 감추었을 뿐이네요. user.newsFeed(subscriptionType)처럼 전략 패턴을 이용해서 값을 직접 전달할 수도 있지요.
페드로: 인정합니다. 상태 패턴은 전략 패턴과 아주 유사하죠. 이 둘은 심지어 UML 다이어ㅓ그램으로 표현할 때도 같은 모양이에요. 하지만 잔액을 캡슐화해서 user 객체 안에 묶어 놓은 것은 다르죠.
이브: 저는 다른 방식을 사용해도 같은 일을 할 수 있다고 생각해요. 그런데 이 방식은, 명시적으로 전략 패턴을 제공하는 대신에, 상태에 의존하는 것이죠. 클로저의 관점에서는 이 패턴을 전략 패턴과 동일한 방법으로 구현할 수 있어요.
페드로: 메소드를 여러 번 호출하면, 객체의 상태가 바뀔 수 있는 데도요?
이브: 맞아요, 하지만 객체의 상태는 전략 패턴과 관련이 없어요. 그것은 단지 구현 상의 한 방법일 뿐이에요.
페드로: 그럼 다른 방식이란 무엇인가요?
이브: 멀티메소드(Multimethods)입니다.
페드로: 멀티 뭐라고요?
이브: 다음 코드를 보세요
(defmulti news-feed :user-state) (defmethod news-feed :subscription [user] (db/news-feed)) (defmethod news-feed :no-subscription [user] (take 10 (db/news-feed)))
이브: 다음의 pay함수는 객체의 상태를 바꾼다는 점을 제외하면 평범한 함수일 뿐이에요. 클로저에서는 상태를 가능한 한 최소화하려 하지만, 필요할 때는 사용해야겠지요.
(def user (atom {:name "Jackie Brown"
:balance 0
:user-state :no-subscription}))
(def ^:const SUBSCRIPTION_COST 30)
(defn pay [user amount]
(swap! user update-in [:balance] + amount)
(when (and (>= (:balance @user) SUBSCRIPTION_COST)
(= :no-subscription (:user-state @user)))
(swap! user assoc :user-state :subscription)
(swap! user update-in [:balance] - SUBSCRIPTION_COST)))
(news-feed @user) ;; top 10
(pay user 10)
(news-feed @user) ;; top 10
(pay user 25)
(news-feed @user) ;; all news
페드로: 멀티메소드를 이용한 디스패치(dispatch)가 enum을 이용한 디스패치보다 더 나은가요?
이브: 이 경우에는 그렇지 않지만, 일반적으로는 그렇습니다.
페드로: 설명해 주시겠어요?
이브: 이중 디스패치(double dispatch)라고 들어 보셨나요?
페드로: 잘 므로겠는데요.
이브: 괜찮아요, 그것이 다음에 다룰 방문자 패턴의 주제이거든요