Skip to content
April 27, 2013 / windperson

用State Pattern解決複雜邏輯判斷

假設在一個線上採購單一物品的網站上,有這樣的折扣規則:

  • 沒有登入會員:
    1. 購買數量五件以上打9折。
    2. 購買數量十件以上打7折。
  • 登入會員:
    1. 每件物品打9折。
    2. 購買數量五件以上打75折。
    3. 購買數量十件以上打5折。

一開始未套用狀態模式的程式碼如下:

public static double CalcTotal(UserData user, int count, double price)
{
    double total = 0;
    if (!user.IsLogin) //not login
    {
        if (count < 5)
            total = count * price;
        else if (count < 10)
            total = count * price * 0.9;
        else if (count >= 10)
            total = count * price * 0.7;
    }
    else //login
    {
        if (count < 5)
            total = count * price * 0.9;
        else if (count < 10)
            total = count * price * 0.75;
        else if (count >= 10)
            total = count * price * 0.5;
    }
    return total;
}

這樣子Procedure-Oriented的寫法,不論是在有登入會員或是沒登入會員部分,計算折扣的程式碼,除了係數值的不同之外,整個邏輯結構是一模一樣的,除了duplicate code會造成需求變更後得同時改好幾個地方的麻煩之外,這種使用連續邏輯判斷結構的程式碼撰寫風格,假如日後對於判斷根據何種情況來套用何種折扣來計算的部分,如因判斷的標準變更,會使得原有的整段邏輯判斷結構程式碼都要被修改,很難保證不會有新增了新功能,卻使得先前運作良好的舊功能壞掉的風險。
要套用狀態模式來重構這段程式碼之前,先來釐清這整個需求的邏輯結構細節,這個針對會員資格和購買商品數量為判斷準則的需求,可以用下面這個狀態圖來表示:
付款模式的狀態圖
在此圖中,圓形的狀態是沒有登入會員的計算折扣狀態,方形的是有登入會員的計算折扣狀態,橫向水平的狀態轉移箭頭是依據目前線上使用者購賣商品的數量,而進入不同的”折扣”狀態來計算應有的打折優惠;縱向垂直的狀態轉移箭頭是根據使用者有沒有登入會員,而產生的狀態變化規則。
在根據狀態圖來套用狀態模式時,務必辨別清楚『狀態』、『狀態轉移路徑』、『外部觸發狀態變化事件』、『內部觸發狀態變化事件』這四個因素在狀態圖內如何被描述。

  • 狀態』在此圖中是圓形和方形的狀態,將會是之後套用狀態模式的程式碼內,AbstractState的子類別所產生的物件。
  • 狀態轉移路徑』在此圖中是水平方向和垂直方向的箭頭,指示如何從某個狀態移轉到另一個狀態,在程式碼實作上,最簡單的情形會寫成固定的判斷邏輯在各個狀態類別的程式碼內;另外有些須達到動態改變狀態轉移路徑的狀態模式實作,會使用另外一個額外的”Lookup Table”資料型態物件儲存狀態轉移路徑的資料,當每次觸發狀態變化事件發生時,程式邏輯會去查詢Lookup Table來得知要跳至的下一個狀態是哪一個,然後再轉移至狀態。
  • 觸發狀態變化事件』有分為內部和外部的,在此例中,使用者有無登入會員,屬於外部觸發狀態變化事件,通常會在狀態模式中的Context類別撰寫額外的公開API呼叫方法來讓client端“手動”叫用,以便將此種事件的資料傳入其中;購買商品數量恰好變動至各個指定的臨界值,屬於內部觸發事件,這類的事件通常是因為Context在接收外部的API呼叫之後,內部成員資料隨之而有改變,假如這變化剛好達到了某些條件而使得Context得自行“主動”狀態轉移,就是內部觸發狀態變化事件,此類動作的程式碼實作通常都直接寫在各個狀態類別之中。

總和以上敘述,實作狀態模式的程式碼如下:

  1. 狀態
    AbstractState抽象類別:
    abstract class AbstractState
    {
        #region State Transition
    
        /// <summary>
        /// Subclass to implement this method to met their
        /// internal state change criterion.
        /// </summary>
        /// <returns>true if the state needs to change, 
        /// otherwise false.</returns>
        protected abstract bool IsStateNeedChange(Context context);
    
        /// <summary>
        /// Subclass may overrides this method to do it's own 
        /// "Change State" implementation when internal state change event occur.
        /// </summary>
        protected abstract void TriggerInternalStateChange(Context context);
    
        /// <summary>
        /// Subclass may overrides this method to do it's own 
        /// "Change State" implementation when internal state change event occur.
        /// </summary>
        /// <param name="context">the context object</param>
        /// <param name="loginFlag">The external event parameter</param>
        protected abstract void TriggerExternalStateChange(Context context, bool loginFlag);
    
        /// <summary>
        /// Verify if state need to change and change if necessary.
        /// </summary>
        /// <param name="context">the context object of State Pattern</param>
        /// <param name="isExternal">true if caller from External Event API</param>
        /// <param name="eventValue">the argument to feed into 
        /// external state change event method:
        ///  <see cref="TriggerExternalStateChange(Context, bool)"/> </param>
        internal void WillStateChange(Context context, bool isExternal = false, bool eventValue = false)
        {
            if (isExternal)
            {
                TriggerExternalStateChange(context, eventValue);
            }
            else
            {
                //because the will buy item may add or remove more than one at a time,
                //it is necessary to check internal state change event more than once.
                while (context.State.IsStateNeedChange(context))
                {
                    context.State.TriggerInternalStateChange(context);
                }
            }
        }
    
        #endregion
    
        /// <summary>
        /// Calculate the total price
        /// </summary>
        /// <param name="context">the context object of State Pattern</param>
        /// <param name="price">each items original price</param>
        /// <returns>the total price after apply discount</returns>
        internal abstract double CalcTotal(Context context, double price);
    }
    

    Normal State: NormalPriceState類別

    class NormalPriceState : AbstractState
    {
        protected override bool IsStateNeedChange(Context context)
        {
            return context.ProductCount >= 5;
        }
    
        protected override void TriggerInternalStateChange(Context context)
        {
            context.State = new Normal10OffState();
        }
    
        protected override void TriggerExternalStateChange(Context context, bool loginFlag)
        {
            if (loginFlag)
                context.State = new Logined10OffState();
        }
    
        internal override double CalcTotal(Context context, double price)
        {
            return context.ProductCount * price;
        }
    }
    

    未登入買五件以上:Normal10OffState類別

    class Normal10OffState : AbstractState
    {
        protected override bool IsStateNeedChange(Context context)
        {
            return context.ProductCount < 5 || context.ProductCount >= 10;
        }
    
        protected override void TriggerInternalStateChange(Context context)
        {
            if (context.ProductCount < 5)
                context.State = new Normal10OffState();
            else if (context.ProductCount >= 10)
                context.State = new Normal30OffState();
        }
    
        protected override void TriggerExternalStateChange(Context context, bool loginFlag)
        {
            if (loginFlag)
                context.State = new Logined25OffState();
        }
    
        internal override double CalcTotal(Context context, double price)
        {
            return context.ProductCount * price * 0.9;
        }
    }
    

    未登入買十件以上:Normal30OffState類別

    class Normal30OffState : AbstractState
    {
        protected override bool IsStateNeedChange(Context context)
        {
            return context.ProductCount < 10;
        }
    
        protected override void TriggerInternalStateChange(Context context)
        {
            context.State = new Normal30OffState();
        }
    
        protected override void TriggerExternalStateChange(Context context, bool loginFlag)
        {
            if (loginFlag)
                context.State = new Logined50OffState();
        }
    
        internal override double CalcTotal(Context context, double price)
        {
            return context.ProductCount * price * 0.7;
        }
    }
    

    登入會員買未滿五件:Logined10OffState類別

    class Logined10OffState : AbstractState
    {
        protected override bool IsStateNeedChange(Context context)
        {
            return context.ProductCount >= 5;
        }
    
        protected override void TriggerInternalStateChange(Context context)
        {
            context.State = new Logined25OffState();
        }
    
        protected override void TriggerExternalStateChange(Context context, bool loginFlag)
        {
            if (!loginFlag)
                context.State = new NormalPriceState();
        }
    
        internal override double CalcTotal(Context context, double price)
        {
            return context.ProductCount * price * 0.9;
        }
    }
    

    登入會員買滿5件以上:Logined25OffState類別

    class Logined25OffState : AbstractState
    {
        protected override bool IsStateNeedChange(Context context)
        {
            return context.ProductCount < 5 || context.ProductCount >= 10;
        }
    
        protected override void TriggerInternalStateChange(Context context)
        {
            if (context.ProductCount < 5)
                context.State = new Logined10OffState();
            else if (context.ProductCount >= 10)
                context.State = new Logined50OffState();
        }
    
        protected override void TriggerExternalStateChange(Context context, bool loginFlag)
        {
            if (!loginFlag)
                context.State = new Normal10OffState();
        }
    
        internal override double CalcTotal(Context context, double price)
        {
            return context.ProductCount * price * 0.75;
        }
    }
    

    登入會員買滿十件以上:Logined50OffState類別

    class Logined50OffState : AbstractState
    {
        protected override bool IsStateNeedChange(Context context)
        {
            return context.ProductCount < 10;
        }
    
        protected override void TriggerInternalStateChange(Context context)
        {
            context.State = new Logined25OffState();
        }
    
        protected override void TriggerExternalStateChange(Context context, bool loginFlag)
        {
            if(loginFlag)
                context.State = new Normal30OffState();
        }
    
        internal override double CalcTotal(Context context, double price)
        {
            return context.ProductCount * price * 0.5;
        }
    }
    
  2. Context類別
    提供所有給client呼叫的API,以及為了使實體狀態類別群的程式碼簡化,除了在計算總價時將計算的邏輯委派給其成員屬性的實體狀態類別實作之外,商品數量的增加與減少之操作,都寫在這個類別:

    class Context
    {
        internal AbstractState State { get; set; }
        private int _amount;
    
        public Context()
        {
            State = new NormalPriceState();
        }
    
        #region Business Domain Public API
    
        public void Add(int number = 1)
        {
            ProductCount += number;
            State.WillStateChange(this);
        }
        public void Remove(int number = 1)
        {
            ProductCount -= number;
            State.WillStateChange(this);
        }
    
        public double CalcTotal(double price)
        {
            return State.CalcTotal(this, price);
        }
    
        /// <summary>
        /// 目前購買商品總數
        /// </summary>
        public int ProductCount
        {
            get
            {
                return _amount;
            }
            protected set
            {
                _amount = value < 0 ? 0 : value;
            }
        }
    
        #endregion
    
        #region External State Change Event
    
        public void Login()
        {
            State.WillStateChange(this, true, true);
        }
    
        public void Logout()
        {
            State.WillStateChange(this, true);
        }
    
        #endregion
    }
    
  3. 狀態轉移路徑、觸發狀態變化事件
    各個狀態的狀態轉移路徑,是在各個實體狀態類別的TriggerInternalStateChange()和TriggerExternalStateChange()實作方法;外部觸發狀態變化事件是由Context類別所提供的這兩個API方法,去直接呼叫AbStractState類別中,WillStateChange()這個各類別要轉換狀態時需執行的共通方法,這個方法會先去檢查是外部狀態轉移還是內部狀態轉移,如果是內部狀態轉移,會在每次轉移後再次檢查是否仍需要再執行轉移狀態的動作,以滿足內部觸發狀態變化事件的狀況。而內部觸發狀態事件就是藉由WillStateChange()內的程式邏輯控制來檢查是否需要狀態轉移。

如何使用這一整個狀態模式的類別組合,以下是範例程式碼:

ouble itemPrice = 15;
Context context = new Context();
 if(user.IsLogin) 
    context.Login();
 else 
    context.Logout();
    
context.Add();
context.Add(2);
context.Remove();
context.Add(3);

Console.WriteLine("Total price={0}", context.CalcTotal(itemPrice));

套用狀態模式後,假如之後需要修改狀態或是狀態轉移路經之間的關係時,由於狀態模式架構上的隔離特性,雖然被修改的狀態類別和其鄰近狀態類別以及對應狀態轉移路徑的程式碼邏輯可能會受影響,但是,其他不相干狀態類別不會被影響到,可將修改程式碼所產生的漣波效應降低。

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: