18、Python與設計模式–訪問者模式

途索發表於2017-02-27

一、藥房業務系統

假設一個藥房,有一些大夫,一個藥品劃價員和一個藥房管理員,它們通過一個藥房管理系統組織工作流程。大夫開出藥方後,藥品劃價員確定藥品是否正常,價格是否正確;通過後藥房管理員進行開藥處理。該系統可以如何實現?最簡單的想法,是分別用一個一個if…else…把劃價員處理流程和藥房管理流程實現,這樣做的問題在於,擴充套件性不強,而且單一性不強,一旦有新藥的加入或者劃價流程、開藥流程有些變動,會牽扯比較多的改動。今天介紹一種解決這類問題的模式:訪問者模式。
首先,構造藥品類和工作人員類:

class Medicine:
    name=""
    price=0.0
    def __init__(self,name,price):
        self.name=name
        self.price=price
    def getName(self):
        return self.name
    def setName(self,name):
        self.name=name
    def getPrice(self):
        return self.price
    def setPrice(self,price):
        self.price=price
    def accept(self,visitor):
        pass
class Antibiotic(Medicine):
    def accept(self,visitor):
        visitor.visit(self)
class Coldrex(Medicine):
    def accept(self,visitor):
        visitor.visit(self)

藥品類中有兩個子類,抗生素和感冒藥;

class Visitor:
    name=""
    def setName(self,name):
        self.name=name
    def visit(self,medicine):
        pass
class Charger(Visitor):
    def visit(self,medicine):
        print "CHARGE: %s lists the Medicine %s. Price:%s " % (self.name,medicine.getName(),medicine.getPrice())
class Pharmacy(Visitor):
    def visit(self,medicine):
        print "PHARMACY:%s offers the Medicine %s. Price:%s" % (self.name,medicine.getName(),medicine.getPrice())

工作人員分為劃價員和藥房管理員。
在藥品類中,有一個accept方法,其引數是個visitor;而工作人員就是從Visitor類中繼承而來的,也就是說,他們就是Visitor,都包含一個visit方法,其引數又恰是medicine。藥品作為處理元素,依次允許(Accept)Visitor對其進行操作,這就好比是一條流水線上的一個個工人,對產品進行一次次的加工。整個業務流程還差一步,即藥方類的構建(流水線大機器)。

class ObjectStructure:
    pass
class Prescription(ObjectStructure):
    medicines=[]
    def addMedicine(self,medicine):
        self.medicines.append(medicine)
    def rmvMedicine(self,medicine):
        self.medicines.append(medicine)
    def visit(self,visitor):
        for medc in self.medicines:
            medc.accept(visitor)

藥方類將待處理藥品進行整理,並組織Visitor依次處理。
業務程式碼如下:

if __name__=="__main__":
    yinqiao_pill=Coldrex("Yinqiao Pill",2.0)
    penicillin=Antibiotic("Penicillin",3.0)
    doctor_prsrp=Prescription()
    doctor_prsrp.addMedicine(yinqiao_pill)
    doctor_prsrp.addMedicine(penicillin)
charger=Charger()
charger.setName("Doctor Strange")
pharmacy=Pharmacy()
pharmacy.setName("Doctor Wei")
doctor_prsrp.visit(charger)
doctor_prsrp.visit(pharmacy)

列印如下:
CHARGE: Doctor Strange lists the Medicine Yinqiao Pill. Price:2.0
CHARGE: Doctor Strange lists the Medicine Penicillin. Price:3.0
PHARMACY:Doctor Wei offers the Medicine Yinqiao Pill. Price:2.0
PHARMACY:Doctor Wei offers the Medicine Penicillin. Price:3.0

二、訪問者模式

訪問者模式的定義如下:封裝一些作用於某種資料結構中的各元素的操作,它可以在不改變資料結構的前提下定義於作用於這些元素的新操作。
f1.png
提到訪問者模式,就不得不提一下雙分派。分派分為靜態分派和動態分派。首先解釋下靜態分派,靜態分派即根據請求者的名稱和接收到的引數,決定多型時處理的操作。比如在Java或者C++中,定義名稱相同但引數不同的函式時,會根據最終輸入的引數來決定呼叫哪個函式。雙分派顧名思義,即最終的操作決定於兩個接收者的型別,在本例中,藥品和工作人員互相呼叫了對方(藥品的accept和工作人員的visit中,對方都是引數),就是雙分派的一種應用。
那麼Python支援靜態分派麼?先看下面的一個例子。

def max_num(x,y,z):
    return max(max(x,y),z)
def max_num(x,y):
    return max(x,y)
if __name__=="__main__":
    print max_num(1,2,4)

列印如下:
Traceback (most recent call last):
File “D:/WorkSpace/Project/PyDesignMode/example.py”, line 786, in

print max_num(1,2,4)

TypeError: max_num() takes exactly 2 arguments (3 given)
可見,Python原生是不支援靜態分派的,因而也不直接支援更高層次的分派。訪問者模式實現的分派,是一種動態雙分派。但這並不妨礙Python通過訪問者模式實現一種基於類的“雙分派效果”。Python多分派可以參考David Mertz 博士的一篇文章:可愛的Python:多分派—用多元法泛化多樣性。

三、訪問者模式的優點和應用場景

優點:
1、將不同的職責非常明確地分離開來,符合單一職責原則;
2、職責的分開也直接導致擴充套件非常優良,靈活性非常高,加減元素和訪問者都非常容易。
應用場景:
1、要遍歷不同的物件,根據物件進行不同的操作的場景;或者一個物件被多個不同物件順次處理的情況,可以考慮使用訪問者模式。除本例外,報表生成器也可以使用訪問者模式實現,報表的資料來源由多個不同的物件提供,每個物件都是Visitor,報表這個Element順次Accept各訪問者完善並生成物件。

四、訪問者模式的缺點

1、訪問者得知了元素細節,與最小隔離原則相悖;
2、元素變更依舊可能引起Visitor的修改。


相關文章