從抽象類開始,詳解責任鏈模式

TechFlow2019發表於2020-10-07

大家好,歡迎大家閱讀設計模式專題。

今天我們繼續介紹新的設計模式,和上次的鏈式模式不同,這一次要介紹的責任鏈模式不僅僅在Python當中有,在很多其他的語言當中同樣支援,比如Java。Python和Java當中的這個設計模式是完全一樣的,沒有任何區別。

和之前介紹的設計模式相比,今天介紹的設計模式同樣不難。我們非常容易想明白它的原理。

責任鏈

在我們日常的開發當中,經常會有上下游的依賴,比如當前我們需要開發一份新的資料,需要用到上游的某一份老資料才能生成。這時候需要我們寫一個介面,然後上游也隨之改造,把它們的新資料寫到我們這個介面來。

這就帶來了一個問題,也就是上下游耦合的問題,如果下游有一天變動了,上游也需要隨著變動。如果是一對一的可能還好,如果一個上游存在多個下游,那麼這會是一件很麻煩的事。每一個下游變動了,上游都需要調整。舉個最簡單的例子,比如下游想要做一個AB測試,想要一半流量進入A機器,一半流量進入B機器。這都需要上游配合。

這當然是很不合理的,在實際當中上游的資料可能是另外一個團隊負責的,他們只對資料質量負責,並不一定會願意配合。並且這樣的配合也會導致效率低下。

為了解決這個問題,有了責任鏈的設計模式。我們把下游的響應方存入一條鏈路當中,上游在傳送請求時不感知下游的接收情況以及相應細節。

說白了,用一句話來概括,就是用鏈路把下游串起來。我們來看一張圖就明白了:

我們把圖中的採購人員看成是資料上游的話,它只管提出請求,具體響應的人他是不知道的。根據申請金額的不同,審批的人員也會不一樣。但是隻會有一個人審批,如果當前人員審批不了就會上報,也就是交由後面一個節點處理。不得不說是非常形象了。

程式碼實現

那怎麼把下游用一條鏈路串起來呢,其實也很簡單,我們可以利用抽象類來實現。比如說我們可以這樣:

class Handler:
    def __init__(self, next=None):
        self.next = next
        
    def handle(self, request):
        res = self.do_handle(request)
        if not res and self.next:
            self.next.handle(request)
            
 def do_handle(self, request):
        pass

這裡的handler就是實現鏈式呼叫的關鍵,在handle當中會首先自己進行處理,如果處理失敗那麼會呼叫下一個handle來進行。Handle類中do_handle的方法是抽象方法,我們希望每一個繼承Handle的子類都能實現自己的do_handle邏輯。

所以如果是用Java來實現的話,會非常清晰,因為在Java當中有抽象類的相關概念。由於Python原生沒有抽象類的相關設定,所以會稍微隱晦一些,但是邏輯是也一樣的。

在我們程式碼實現之前,我們先來介紹一個無關緊要的包。這個包就是Python當中的six,其實這個包也不算是無關緊要,通過它可以寫出相容Python2和Python3的程式碼。但是實際上由於現在Python2已經快掃進歷史的垃圾堆了,所以有沒有這個包都沒有關係,這裡我們還是尊重原作者的程式碼,保留了這麼一個包。我們用這個包作為註解,完成了抽象基類的構建。

抽象基類

首先,我們先來看這個抽象基類。在原生Python當中,其實是沒有抽象基類這麼一個概念的。抽象基類其實就是含有抽象方法的基類。

我們之前在介紹golang當中interface用法的時候曾經介紹過,我們來簡單回憶一下。拿比較常見的Java語言舉例。

比如說這是一個抽象基類:

abstract class Mammal {
    abstract void say();
    String name() {
        return "chengzhi";
    }
}

對於這個類而言它是不可以直接建立例項的,因為我們可以看出來它有一個什麼也沒實現的方法say。這個方法前面加了一個關鍵字abstract即抽象的意思,表示這是一個抽象方法。類名的前面同樣加了這個關鍵字,表示這是一個抽象類。

對於抽象類我們不能直接建立它的例項,我們只能建立實現了抽象類中抽象方法的子類的例項。

比如我們建立一個類實現它:

class Human extends Mammal{
    @override
 public void say() {
  System.out.println("perfect");
 }
}

我們可以看出來Human這個類繼承了Mammal這個抽象類,並且實現了抽象類當中的方法say。所以我們可以建立它的例項。抽象類是實現多型的一種重要的方法,在強變數型別的語言當中,我們通過抽象類抽象出了多個子類共同的結構。這樣我們就可以通過父類的指標呼叫各種子類的物件了,這是非常方便的。

但是Python當中不支援,為什麼Python不支援呢?其實也很簡單,因為Python是弱變數型別語言。變數賦值的時候對於型別根本沒有限制,我們可以做任何呼叫。

舉個例子,比如我們當下有A、B和C這三個類的例項。哪怕這三個類毫無關係,我們也可以用一個變數去分別接收這些例項然後呼叫同名的方法。

a = [A(), B(), C()]
for i in a:
    i.say()

只要A、B和C這三個類當中都有叫做say的方法,那麼上面這段邏輯就不會報錯。因此Python非常靈活,可以說是幾乎沒有限制,那麼當然也就沒有必要設計這麼一個抽象類的設定了。

但問題是我們開發的時候並不是這樣的,比如說我們在設計的時候希望能夠讓類之間有繼承的關係,並且希望繼承了某個父類的類都能實現某一個方法。由於Python原生沒有相關的設定,導致我們很難在編碼的排查錯誤。比如說我們忘記實現了某個方法,由於Python沒有編譯的過程,我們只有執行到報錯的地方才會遇到問題,這顯然是不行的,會有很大的隱患。

所以Python開發者想了一個辦法就是設計了一個裝飾器,叫做abc。這裡的abc不是隨意起的名字,其實是abstract basement class的縮寫。加上了這個裝飾器之後,它會替我們去檢查所有子類有沒有實現抽象方法。如果沒有的話,那麼會報錯提示我們。

我們用上six和abc這兩個包之後實現出來的基類是這樣的:

import abc
import six


@six.add_metaclass(abc.ABCMeta)
class Handler(object):

    def __init__(self, successor=None):
        self.successor = successor

    def handle(self, request):
        res = self.check_range(request)
        if not res and self.successor:
            self.successor.handle(request)

    # 抽象方法
    @abc.abstractmethod
    def check_range(self, request):
        pass

實現類

我們把抽象類相關的概念以及責任鏈的原理理解了之後,就可以很簡單地寫出來實現類了。

所謂的實現類也就是抽象類的子類,實現了各自的抽象方法。這樣我們在每個節點當中呼叫self.successor.handle的時候才可以執行。因為每一個successor都是繼承了同樣一個抽象類的實現類,它們都必然實現了handle這個抽象方法。所以整個呼叫的過程就像是鏈條一樣串聯了起來。

在這個例子當中,我們用數字表示每個handler的處理範圍。只有落在範圍裡的請求才會被響應,如果當前handler無法響應,那麼就會呼叫successor的handle繼續嘗試。

class ConcreteHandler(Handler):

    @staticmethod
    def check_range(request):
        if 0 <= request < 10:
            print('request {} handled in handler 0'.format(request))
            return True

    
class ConcreteHandler1(Handler):

    start, end = 10, 20

    def check_range(self, request):
        if self.start <= request < self.end:
            print('request {} handled in handler 1'.format(request))
            return True


class ConcreteHandler2(Handler):

    def check_range(self, request):
        start, end = self.get_interval()
        if start <= request < end:
            print('request {} handled in handler 2'.format(request))
            return True

    @staticmethod
    def get_interval():
        return (20, 30)


class FallBackHandler(Handler):

    @staticmethod
    def check_range(request):
        print('end of chain, no handler for {}'.format(request))
        return False

這裡要注意一下FallBackHandler這個類,這個類表示責任鏈的結尾,也就是它是最後一個節點。如果請求一直穿過前面的節點送到了它這裡,就表示前面沒有相應的handler,這個請求無法響應。

最後我們來看下實際執行的例子:

if __name__ == '__main__':
    h0 = ConcreteHandler()
    h1 = ConcreteHandler1()
    h2 = ConcreteHandler2(FallBackHandler())
    h0.successor = h1
    h1.successor = h2

    requests = [2, 5, 14, 22, 18, 3, 35, 27, 20]
    for x in requests:
        h0.handle(x)

我們可以看到我們建立了一系列handler的例項,然後將它們串聯了起來。如果我們執行這段程式碼的話會發現我們不同的請求被不同的handler響應了。

到這裡關於責任鏈這個設計模式的介紹就結束了,這個設計模式非常巧妙,將上游和下游分裂開來,彼此之間消除了耦合。但是這也並不是完美的,它也有自己的缺點,一個很明顯的缺點就是這樣的效率會比較低。因為一層層鏈路傳遞都是需要時間的,尤其是這些鏈路如果還要遠端呼叫的話,那麼每一層轉換都會有延遲以及相應時間,所以整個系統的效率可能會不高。

還有呢就是我們除錯的時候可能會不太方便,錯誤不太容易排查。除此之外還有一些其他的優缺點,大家可以自己思考一下,在下方留言。

衷心祝願大家每天都有所收穫。如果還喜歡今天的內容的話,請來一個三連支援吧~(點贊、關注、轉發

原文連結,求個關注

本文使用 mdnice 排版

- END -

相關文章