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)라고 들어 보셨나요?
페드로: 잘 므로겠는데요.
이브: 괜찮아요, 그것이 다음에 다룰 방문자 패턴의 주제이거든요