[ASP.NET]重構之路系列v10 –責任鏈模式的應用

[ASP.NET]重構之路系列v10 –責任鏈模式的應用

前言
今天這篇主要要說明的,是責任鏈模式的應用(要說變形也可以)。

責任鏈的精神,簡單的說,就是:『阿星,接力!接力!』就像接力賽一樣,把一陀事情抽象來看,轉成對應的物件所擁有的抽象方法,並由使用場景定義好,這些物件的接棒順序。

整個物件的合作過程,就像安排大隊接力一樣,每個物件就像每個選手,只要負責用自己的方式,跑完自己的責任,並察看是不是有下一個接棒者,若有則交棒。

需求說明

  1. 背景是應徵者來面試,依據應徵者應徵職位的不同,來決定面試官的組成與面試的順序。
  2. 每一個面試官面試的方式跟評分的重點不一樣。
  3. 未來可能會有調整面試官組成或面試順序的情況。
  4. 未來開放的職缺職位也可能會增加,相對應的面試官團隊也會增加或不同。
  5. 每個面試官獨自的面試方式或評分的重點也可能會改變。


舉例

  1. 應徵的職位為RD,其面試官組成與順序:HR→Team Leader→Manager。
  2. 應徵的職位為Manager,其面試官組成與順序:HR→Manager→VP。
  3. 面試過程中,面試官有權力終止面試流程,若無問題,則送應徵者往下一關卡。


原始的設計
先將會用到的類別程式碼列出:

    /// <summary>
    ///  應徵者介面
    /// </summary>
    public interface ICandidate
    {
        /// <summary>
        /// Gets or sets a value indicating whether this instance is lost qualification.
        /// 是否失去資格,代表不用往下再面試了
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is lost qualification; otherwise, <c>false</c>.
        /// </value>
        bool IsLostQualification { get; set; }

        /// <summary>        
        /// 人格特質評分
        /// </summary>
        /// <value>
        /// The personality points.
        /// </value>
        int PersonalityPoints { get; set; }

        /// <summary>
        /// 技術能力評分
        /// </summary>
        /// <value>
        /// The technical ability points.
        /// </value>        
        int TechnicalAbilityPoints { get; set; }

        /// <summary>
        /// 態度評分
        /// </summary>
        /// <value>
        /// The attitude points.
        /// </value>
        int AttitudePoints { get; set; }

        /// <summary>
        /// 潛力評分
        /// </summary>
        /// <value>
        /// The potential points.
        /// </value>
        int PotentialPoints { get; set; }

        /// <summary>
        /// Gets or sets the interview result.
        /// </summary>
        /// <value>
        /// The interview result.
        /// </value>
        HireStatus InterviewResult { get; set; }

        /// <summary>
        /// Gets or sets the position.
        /// </summary>
        /// <value>
        /// The position.
        /// </value>
        Position Position { get; set; }

        /// <summary>
        /// 薪資
        /// </summary>
        /// <value>
        /// The payment.
        /// </value>
        long Payment { get; set; }

        /// <summary>
        /// 評估與結算,應徵者的面試結果、薪資狀態等等...
        /// </summary>
        void CalculateResult();
    }

    public class Candidate : ICandidate
    {
        public bool IsLostQualification { get; set; }

        public int PersonalityPoints { get; set; }

        public int TechnicalAbilityPoints { get; set; }

        public int AttitudePoints { get; set; }

        public int PotentialPoints { get; set; }

        public HireStatus InterviewResult { get; set; }

        public Position Position { get; set; }

        public long Payment { get; set; }

        public void CalculateResult()
        {
            //可再透過strategy pattern來決定不同的職務該怎麼決定面試結果
            switch (this.Position)
            {
                case Position.RD:
                    this.CalculateRD();
                    break;
                case Position.Manager:
                    this.CalculateManager();
                    break;
                default:
                case Position.None:
                    break;
            }
        }

        private void CalculateManager()
        {
            double points = (this.AttitudePoints + this.PersonalityPoints + this.PotentialPoints) / (double)3;

            if (this.PersonalityPoints < 60 || points < 60)
            {
                this.InterviewResult = HireStatus.Reject;
            }
            else if (points >= 60 && points < 80)
            {
                this.InterviewResult = HireStatus.SecondRound;
            }
            else if (points > 90)
            {
                this.Payment = 100;
                this.InterviewResult = HireStatus.Hire;
            }
            else
            {
                this.InterviewResult = HireStatus.WaitForOtherCandidate;
            }
        }

        private void CalculateRD()
        {
            if (this.TechnicalAbilityPoints < 60)
            {
                this.InterviewResult = HireStatus.Reject;
            }
            else
            {
                this.InterviewResult = HireStatus.Hire;
                this.Payment = 60;
            }
        }
    }

    public enum HireStatus
    {
        /// <summary>
        /// 初始值
        /// </summary>
        None = 0,

        /// <summary>
        /// 不予錄取
        /// </summary>
        Reject,

        /// <summary>
        /// 錄取
        /// </summary>
        Hire,

        /// <summary>
        /// 轉介其他部門
        /// </summary>
        ForwardOtherDepartment,

        /// <summary>
        /// 第二輪面試機會
        /// </summary>
        SecondRound,

        /// <summary>
        /// 等待其他應徵者面試完畢比較
        /// </summary>
        WaitForOtherCandidate
    }

    public enum Position
    {
        None = 0,
        RD,
        Manager
    }


1.使用巢狀的if/else if來判斷應徵者的應徵職務,將每個面試官的面試方式獨立成分開的function,以便抽象的描述、設計與重用。
2.每個面試官面試完,要判斷是否要終止面試過程,還是程式碼要繼續往下執行。

缺點:
1.巢狀if/else if,可讀性差,擴充性差。
2.看不出來所謂的『面試流程』,只看的到一堆判斷。
3.未來新增應徵職位或希望改變順序,都是一件困難的事。

 

    class OriginalCode
    {
        static void Main(string[] args)
        {
            ICandidate candidate = new Candidate() { Position = Position.RD };

            ICandidate result = Interview(candidate);
        }

        /// <summary>
        /// 如果是來應徵RD的,那要先經過HR, Team Leader, Manager面試。若面試過程中,出現大問題,面試官有權終止面試流程。若沒大問題,則送應徵者往下一關前進。
        /// 如果是來應徵Manager的,那要經過HR, Manager, VP的面試。若面試過程中,出現大問題,面試官有權終止面試流程。若沒大問題,則送應徵者往下一關前進。
        /// 每個關卡面試的邏輯都不一樣
        /// 不同的職務,會有不同的面試流程
        /// 未來則可能依據特殊狀況來調整面試的順序或是面試官的角色,例如RD先與Manager面試,再與Team Leader面試。或是高階RD還需要通過VP面試等等...
        /// </summary>
        /// <param name="candidate"></param>
        /// <returns></returns>
        private static ICandidate Interview(ICandidate candidate)
        {
            var result = candidate;

            if (candidate.Position == Position.RD)
            {
                result = InterviewByHR(result);
                if (!result.IsLostQualification)
                {
                    result = InterviewByTeamLeader(result);
                    if (!result.IsLostQualification)
                    {
                        result = InterviewByManager(result);
                    }
                }

                return result;
            }
            else if (candidate.Position == Position.Manager)
            {
                result = InterviewByHR(result);
                if (!result.IsLostQualification)
                {
                    result = InterviewByManager(result);
                    if (!result.IsLostQualification)
                    {
                        result = InterviewByVP(result);
                    }
                }
                return result;
            }

            return result;
        }

        private static ICandidate InterviewByVP(ICandidate result)
        {
            throw new NotImplementedException();
        }

        private static ICandidate InterviewByManager(ICandidate result)
        {
            throw new NotImplementedException();
        }

        private static ICandidate InterviewByTeamLeader(ICandidate result)
        {
            throw new NotImplementedException();
        }

        private static ICandidate InterviewByHR(ICandidate result)
        {
            throw new NotImplementedException();
        }
    }

抽象的分離職責
1.將每一個角色拆成獨立的物件,並予以相同的操作:interview。
2.每一個角色在interview後,會檢查是否將應徵者送往下一關卡,以及是否有下一關卡。

來看一下,應用責任鏈模式來設計這個case,其Class Diagram應該像這樣:
Class Diagram

  1. 每一個面試官都是一種Interviewer。
  2. Interviewer都有一個Interview的方法,但這個interview的方法由各個子類自行決定內容,故interview()宣告成abstract。
  3. Interviewer將應徵者送往下一關卡的動作,宣告成GoNext()方法,且每一個面試官都應該依循同一個準則來執行。
  4. 在決定Interviewer的時候,應決定下一個負責的Interviewer是哪個角色。也就是圖上AbstractInterviewer自己與自己的聚合關係。
  5. 由一個中介的建構類來決定,面試官的組成與順序。這邊為簡單工廠模式,但其實改為使用Builder會更恰當(這就當下一篇文的伏筆吧…)
  6. Client,也就是使用場景,則應只需將應徵者ICandidate丟給建構類回傳的AbstractInterviewer,呼叫interview的方法即可。
     

使用責任鏈模式應用來重構
1.抽象的面試官(AbstractInterviewer):將面試官的行為抽象出來

 

    public abstract class AbstractInterviewer
    {
        /// <summary>
        /// 定義下一個interviewer
        /// </summary>
        /// <value>
        /// The next interviewer.
        /// </value>
        protected AbstractInterviewer NextInterviewer { get; set; }

        public AbstractInterviewer()
        {

        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AbstractInterviewer"/> class.
        /// 若傳入為null,則代表是最後一關
        /// </summary>
        /// <param name="nextInterviewer">The next interviewer.</param>
        public AbstractInterviewer(AbstractInterviewer nextInterviewer)
        {
            this.NextInterviewer = nextInterviewer;
        }

        /// <summary>
        /// 每一個子類interview的內容
        /// </summary>
        /// <param name="candidate">The candidate.</param>
        /// <returns></returns>
        public abstract ICandidate InterView(ICandidate candidate);

        /// <summary>
        /// 往下一個interview關卡
        /// </summary>
        /// <param name="candidate">The candidate.</param>
        /// <returns></returns>
        protected ICandidate GoNext(ICandidate candidate)
        {
            var result = candidate;

            ////還有下一個interviewer且應徵人還沒失去資格時,送應徵者往下一個interview關卡
            if (this.NextInterviewer != null && !result.IsLostQualification)
            {
                result = this.NextInterviewer.InterView(candidate);
            }

            result.CalculateResult();

            return result;
        }
    }

2. Concrete面試官(包括HR, TeamLeader, Manager, VP):用來決定各個角色面試方式與評分標準

    public class HR : AbstractInterviewer
    {
        public HR(AbstractInterviewer nextInterviewer)
        {
            this.NextInterviewer = nextInterviewer;
        }

        /// <summary>
        /// HR interview的邏輯與內容
        /// </summary>
        /// <param name="candidate">The candidate.</param>
        /// <returns></returns>
        public override ICandidate InterView(ICandidate candidate)
        {
            candidate.PersonalityPoints = 60;
            Console.WriteLine("{0}面試中...人格特質:{1}", "HR", candidate.PersonalityPoints.ToString());

            return this.GoNext(candidate);
        }
    }

 

    public class TeamLeader : AbstractInterviewer
    {
        public TeamLeader(AbstractInterviewer nextInterviewer)
        {
            this.NextInterviewer = nextInterviewer;
        }

        /// <summary>
        /// TeamLeader interview的邏輯與內容
        /// </summary>
        /// <param name="candidate">The candidate.</param>
        /// <returns></returns>
        public override ICandidate InterView(ICandidate candidate)
        {
            candidate.TechnicalAbilityPoints = 70;
            Console.WriteLine("{0}面試中...技術能力:{1}", "TeamLeader", candidate.TechnicalAbilityPoints.ToString());
            return this.GoNext(candidate);
        }
    }

 

    public class Manager : AbstractInterviewer
    {
        public Manager(AbstractInterviewer nextInterviewer)
        {
            this.NextInterviewer = nextInterviewer;
        }

        /// <summary>
        /// Manager interview的邏輯與內容
        /// </summary>
        /// <param name="candidate">The candidate.</param>
        /// <returns></returns>
        public override ICandidate InterView(ICandidate candidate)
        {
            candidate.PotentialPoints = 60;
            Console.WriteLine("{0}面試中...潛力分數:{1}", "Manager", candidate.PotentialPoints.ToString());
            return this.GoNext(candidate);
        }
    }

 

    public class VP : AbstractInterviewer
    {
        public VP(AbstractInterviewer nextInterviewer)
        {
            this.NextInterviewer = nextInterviewer;
        }

        /// <summary>
        /// VP interview的邏輯與內容
        /// </summary>
        /// <param name="candidate">The candidate.</param>
        /// <returns></returns>
        public override ICandidate InterView(ICandidate candidate)
        {
            candidate.AttitudePoints = 80;
            Console.WriteLine("{0}面試中...態度分數:{1}", "VP", candidate.AttitudePoints.ToString());
            return this.GoNext(candidate);
        }
    }

3. 簡單工廠(FactoryMethod):依據應徵職位,決定面試官的組成與面試順序

    public class FactoryMethod
    {
        /// <summary>
        /// 依據不同職位來安排不同的面試關卡        
        /// </summary>
        /// <param name="position">The position.</param>
        /// <returns></returns>
        public static AbstractInterviewer GetInterviewers(Position position)
        {
            switch (position)
            {
                //如果應徵的職位是RD,面試關卡為:HR=>TeamLeader=>Manager
                case Position.RD:
                    return new HR(new TeamLeader(new Manager(null)));
                //如果應徵的職位是Manager
                case Position.Manager:
                    return new HR(new Manager(new VP(null)));
                default:
                case Position.None:
                    return null;
            }
        }
    }

4. 重構後的使用場景

    class Program
    {
        /// <summary>
        /// InterviewManager()與InterviewRD(),也可以改用strategy pattern來實作。        
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            InterviewManager();
            Console.WriteLine();

            InterviewRD();
            Console.ReadLine();
        }

        private static void InterviewRD()
        {
            ICandidate candidate = new Candidate { Position = Position.RD };

            Interview(candidate);
        }

        private static void InterviewManager()
        {
            ICandidate candidate = new Candidate { Position = Position.Manager };

            Interview(candidate);
        }

        private static void Interview(ICandidate candidate)
        {
            //簡單工廠模式可以改用builder pattern來實作更為恰當
            var interviewer = FactoryMethod.GetInterviewers(candidate.Position);
            interviewer.InterView(candidate);

            Console.WriteLine(@"應徵職缺:{0}, 面試結果:{1}", candidate.Position.ToString(), candidate.InterviewResult.ToString());

            if (candidate.InterviewResult == HireStatus.Hire)
            {
                Console.WriteLine("薪資:{0}萬", candidate.Payment.ToString());
            }
        }
    }

結果
image

結論
這一篇的應用,其實也不是所謂正規的責任鏈模式,但class diagram與pattern的設計是一致的,要解決的問題也是一致的,只是根據特有的需求來加以變化。

透過責任鏈來設計後,我們滿足了前面列的需求說明:

  1. 讓每個角色各司其職,不用管前後是誰,只要面試完,交給下一關卡。
  2. 讓使用場景不需知道面試細節,甚至不需知道面試官的組成與順序。使用場景focus的只有,把應徵者推去面試。
  3. 職位與面試官組成/順序的關係獨立出來,與面試細節無關,與使用場景無關。


責任鏈雖帶來以上的彈性,但也是有缺點的,責任鏈其實有點像是物件之間的方法遞迴(實際偵錯run一輪就會很有感覺了),所以責任鏈越長,可能效率會越差。


後記
寫Design Pattern文章有幾個很麻煩的地方:一個Pattern,通常是針對特定的問題/需求,累積了許多的經驗所定義出比較common的設計方式來解決,所以稱為Pattern。
如果眼前或未來沒有這樣的問題或需求,那用了只是增加複雜度,也就是所謂的over design。

而針對一個Pattern寫的文章,就得凸顯這一類的問題與需求,針對這個需求來設計sample與應用pattern後的差異。但如果一篇文章用到多個Pattern,就很容易讓讀者搞混。所以比較好懂的表達方式是,只重構該問題領域與該Pattern所屬範圍,這樣才能凸顯該Pattern的重點。

所以,很多文章,不是因為作者不懂,或是為什麼sample code有些地方明明可以設計的更漂亮,卻不用其他pattern去設計或重構。原因只是:那不是我這個Pattern要強調的重點,且我得假設眼前與未來沒有那樣的需求。

只有讀者一個一個pattern去理解,並且實際地碰到該類問題/需求,實際的去實作與設計,才能體會為什麼需要Design Pattern。所以,請饒了我吧,如果一篇文章一個應用,隨手就放了四個Pattern,那要嘛文章長到爆炸,要嘛沒人願意看完或看懂。

Sample Project:ChainOfResponsibilitySample.zip


blog 與課程更新內容,請前往新站位置:http://tdd.best/