iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 5
2
Software Development

Learning Design Pattern in 30 real-case practices系列 第 5

職場第一條規則:事情不要接了傻傻做,而是要交給負責的人! (Chain of Responsibility 職責鍊模式)

  • 分享至 

  • xImage
  •  

Chain of Responsibility 職責鍊模式

需求描述

Amy(PO):

As a 產品經理
I want 多國語系介面
So that 顯示使用者對應的語系內容。

思考設計

Hachi:
我看了一下DoD(Definition of Done),系統需要支援繁、簡中和英文。 所以只要前端能丟給我一個語系的參數,我就能從資料庫抓出對應的語系內容。 我只是對於這段Switch判斷有些疑問... (指了以下程式碼)

var content = new Content();
switch(localization)
{
    case "zh-TW":
       content.City = DataFactory.CityZh;
       content.Country = DataFactory.CountryZh;
       break;
    case "zh-CN":
       content.City = DataFactory.CityCn;
       content.Country = DataFactory.CountryCn;
       break;
    default:
       content.City = DataFactory.CityEn;
       content.Country = DataFactory.CountryEn;
       break;
}

JB:
恩,我看不出來有什麼問題耶?

Hachi:
程式碼太長了;而且未來如果我們需要支援更多語系,這個Switch會越來越龐大。

JB:
我很想幫你,可是我現在得去開一個ERP的會議。天啊! 他們總是搞不清楚職責,ERP不是我們Team負責的,他們應該丟給...

Hachi:
等等! 你提到了"職責"? 感謝你! 我想可以用職責鍊模式來重構這段程式碼!

定義

根據條件來定義一個有責任的接收者,以處理一個請求或將其轉發給鏈上的下一個接收者(如果有的話)(WIKI)

Chain of Responsibility(職責鍊)大概是我們最容易理解的設計模式,因為我們每天都想盡辦法在實現它。
例如:

老闆交代了一件工作給PM =>
PM判斷此事優先權極高,立即跟資深工程師說有插件,做吧! =>
資深工程師判斷此事不急且快下班了,說我有事,菜鳥做吧! =>
菜鳥工程師看了一下沒人可以往後丟了,摸摸鼻子做事去了 => 結束

開玩笑地,相信大家的團隊應該是這樣:

老闆交代了一件工作給PM =>
PM整理了一些資訊,再交給資深工程師 =>
資深工程師判斷此事極容易所以直接寫完程式碼並做單元測試 => 結束

也就是說在每個Chain上的接收者可以選擇直接丟給下一個適合的接收者,或是選擇做事再判斷適合的接收者後往後丟
我們要練習的多國語系案例比較偏向不符合我的職責就往後丟的情況。
其實職責鍊的用法非常多,我們的範例算是相對簡單的用法。

建立IHandler介面和職責鍊的頭(大家有事先找PM處理的概念):Handler
注意PM的職責是什麼?把東西往後丟嘛,不過丟給誰我們等下再來決定。

  • C#
public interface IHandler
{
    IHandler Next { get; }
    Content Action(string localization);
}

public class Handler : IHandler
{
    public virtual IHandler Next { get; set; }

    public virtual Content Action(string localization)
    {
        return this.Next.Action(localization);
    }
}
  • Python
from abc import ABC, abstractmethod

class HandlerBase(ABC):    

    @property
    @abstractmethod
    def next(self):
        pass

    @next.setter
    @abstractmethod
    def next(self, val):
        pass

    @abstractmethod
    def action(self, localization):
        pass
        

class Handler(HandlerBase):
    _next = None
    
    @property
    def next(self):
        return self._next

    @next.setter
    def next(self, val):
        self._next = val

    def action(self, localization):
        if (self._next == None):
            self._next = ReceiverZh()

        return self._next.action(localization)

下一步開始定義實際作業的Receiver類別(苦命工程師?),一樣是實作IHandler,當然你也可以選擇繼承Handler再覆寫。
注意我們要在每個Receiver裡面再宣告一個職責鍊的下一個Receiver,在目前作業完成後去呼叫下一個Receiver的作業。
這樣整個職責鍊就開始串起來了!

  • C#
public class ReceiverZh : IHandler
{
        public IHandler Next { get; set; } //Next Receiver

        public Content Action(string localization)
        {
            if (localization.Equals("zh-TW"))
            {
                var content =  new Content{
                    Country=DataFactory.CountryZh,
                    City=DataFactory.CityZh
                };
                return content;
            }
            else
                System.Diagnostics.Trace.WriteLine($"Not zh-TW, go to next receiver...");
            
            #region Do next
            if (this.Next == null) //Set a default next receiver
                this.Next = new ReceiverCn();

            return this.Next.Action(localization);
            #endregion
        }
}
  • Python
class ReceiverZh(HandlerBase):
    _next=None
    
    @property
    def next(self):
        return self._next

    @next.setter
    def next(self, val):
        self._next = val        

    def action(self, localization):
        #Action
        if (localization=="zh-TW"):
            content =  Content(DataFactory.countryZh(),DataFactory.cityZh())
            print("{0} {1}".format(content.country, content.city))
            return content
        else:
            print("Not zh-TW, go to next receiver...")

        #Go to next
        if (self._next == None):
            self._next = ReceiverCn()

        return self._next.action(localization)

接下來請依樣畫葫驢建立ReceiverCn,ReceiverEn類別,並且設定預設的職責鍊順序為:
Handler -> ReceiverZh -> ReceiverCn -> ReceiverEn -> ReceiverException

  1. 為什麼需要最後一個ReceiverException
    因為老闆總是會隨便亂丟不屬於我們職責的工作進來>.< 我們職責鍊無法處理的事情,那就要說出來讓老闆知道:
    "你得多派一個專職的人來處理(多建立一個Receiver),這事才能成!"

  2. 職責鍊的順序是可以調整的,下面有一個範例說明。

我們來看主程式如何使用職責鍊。

  • C#
string localization="zh-CN";
var handler = new Handler();
var content = handler.Action(localization);
  • Python
localization="zh-CN"
handler = Handler()
content = handler.action(localization)

上面執行的結果會是

Not zh-TW, go to next receiver...
台湾 台东市

原本的Switch程式碼被重構成幾行解決,是不是開始感受設計模式的魅力了呢?
另外不使用預設的職責鍊順序,也可以自行指定。
例如以下的範例,其職責鍊順序我們調整為 HandlerEn -> HandlerZh -> HandlerException
注意順序要指定好,避免無窮迴圈。

  • C#
string localization="zh-TW";

var handlerEn = new ReceiverEn();
var handlerZh = new ReceiverZh();
var handlerFinal = new ReceiverException();

handlerEn.Next = handlerZh;
handlerZh.Next = handlerFinal;

var content = handlerEn.Action(localization);
  • Python
localization="zh-TW"

handlerEn = ReceiverEn()
handlerZh = ReceiverZh()
handlerFinal = ReceiverException()

handlerEn.next = handlerZh
handlerZh.next = handlerFinal
    
content = handlerEn.action(localization)

Sample Codes

  1. C#
  1. Python

Reference


上一篇
雞同鴨講也可以通! (Interpreter 解譯器模式)
下一篇
不用看書就會,但不一定會唸的... (Facade 外觀模式)
系列文
Learning Design Pattern in 30 real-case practices30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言