大家好,歡迎大家閱讀設計模式專題。
今天我們繼續介紹新的設計模式,和上次的鏈式模式不同,這一次要介紹的責任鏈模式不僅僅在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 -