Go 設計模式 -- 訪客模式
訪客模式也叫訪問者模式(Visitor Pattern)是一種將數據結構對象與數據操作分離的設計模式,可以在不改變數據結構對象類結構的前提下定義作用於這些對象的新的操作, 屬於行爲型設計模式。
訪問者模式主要適用於以下應用場景:
-
數據結構穩定,作用於數據結構的操作經常變化的場景。
-
需要數據結構與數據操作分離的場景。
-
需要對不同數據類型(元素)進行操作,而不使用分支判斷具體類型的場景。
訪客模式怎麼工作?
訪問者模式通過將算法與對象結構分離來工作,這裏說的算法指的是對對象的操作。爲此,我們需要定義了一個表示算法的接口 --Visitor。該接口將爲對象結構中的每個類(一般稱爲元素類)提供一個方法。每個方法都將元素類的一個實例作爲參數。表示對象結構的所有元素類也會實現一個 Element 接口,該接口定義了接受訪問者的方法 Accpet。此方法將訪問者接口的實現作爲參數。當 Accpet 方法被調用時,訪問者實例對應的方法就會被調用,通過訪問者完成對元素類實例的操作。
下面我們看一下訪問者模式的類結構。
訪客模式結構
訪問者的類結構可以用下面的 UML 類圖來表示:
-
訪客接口 (Visitor) 聲明瞭一系列以表示對象結構的具體元素爲參數的訪問者方法。 如果編程語言支持重載, 這些方法的名稱可以是相同的, 但是其參數一定是不同的。
-
具體訪客 (Concrete Visitor) 會爲不同的具體元素類實現相同行爲的幾個不同版本。
-
元素 (Element) 接口,聲明瞭一個方法來 “接收” 訪問者。 該方法必須有一個參數被聲明爲訪問者接口類型。
-
具體元素 (Concrete Element) 必須實現接收方法。 該方法的目的是根據當前元素類將其調用重定向到相應訪問者的方法。 請注意, 即使元素基類實現了該方法, 所有子類都必須對其進行重寫並調用訪客對象中的合適方法。
訪客模式代碼示例
在這個用訪客模式實現不同維度的訂單統計的例子裏,假設我們建設了一個訂單管理系統, 現在系統中要求能按照不同維度統計分析銷售訂單
-
區域銷售報表: 需按銷售區域, 統計銷售情況
-
品類銷售報表: 需根據不同產品, 統計銷售情況
以後還有可能增加其他維度的銷售統計報表,針對這個需求我們可以根據訪問者模式, 可將不同的報表, 設計爲訂單的訪問者。 首先定義訂單實體和它要實現的 Element 接口
"本文使用的完整可運行源碼
去公衆號「網管叨bi叨」發送【設計模式】即可領取"
// 訂單服務接口
type IOrderService interface {
Save(order *Order) error
// 有的教程裏把接收 visitor 實現的方法名定義成 Accept
Accept(visitor IOrderVisitor)
}
// 訂單實體類,實現IOrderService 接口
type Order struct {
ID int
Customer string
City string
Product string
Quantity int
}
func (mo *OrderService) Save(o *Order) error {
mo.orders[o.ID] = o
return nil
}
func (mo *OrderService) Accept(visitor IOrderVisitor) {
for _, v := range mo.orders {
visitor.Visit(v)
}
}
func NewOrder(id int, customer string, city string, product string, quantity int) *Order {
return &Order{
id, customer,city,product,quantity,
}
}
接下來定義生成各種銷售報表的訪客類,以及它們實現的訪客接口
"本文使用的完整可運行源碼
去公衆號「網管叨bi叨」發送【設計模式】即可領取"
type IOrderVisitor interface {
// 這裏參數不能定義成 IOrderService
Visit(order *Order)
Report()
}
type CityVisitor struct {
cities map[string]int
}
func (cv *CityVisitor) Visit(o *Order) {
n, ok := cv.cities[o.City]
if ok {
cv.cities[o.City] = n + o.Quantity
} else {
cv.cities[o.City] = o.Quantity
}
}
func (cv *CityVisitor) Report() {
for k,v := range cv.cities {
fmt.Printf("city=%s, sum=%v\n", k, v)
}
}
func NewCityVisitor() IOrderVisitor {
return &CityVisitor{
cities: make(map[string]int, 0),
}
}
// 品類銷售報表, 按產品彙總銷售情況, 實現ISaleOrderVisitor接口
type ProductVisitor struct {
products map[string]int
}
func (pv *ProductVisitor) Visit(it *Order) {
n,ok := pv.products[it.Product]
if ok {
pv.products[it.Product] = n + it.Quantity
} else {
pv.products[it.Product] = it.Quantity
}
}
func (pv *ProductVisitor) Report() {
for k,v := range pv.products {
fmt.Printf("product=%s, sum=%v\n", k, v)
}
}
func NewProductVisitor() IOrderVisitor {
return &ProductVisitor{
products: make(map[string]int,0),
}
}
最後我們嘗試使用 Vistor 生成各種銷售報表
func main() {
orderService := NewOrderService()
orderService.Save(NewOrder(1, "張三", "廣州", "電視", 10))
orderService.Save(NewOrder(2, "李四", "深圳", "冰箱", 20))
orderService.Save(NewOrder(3, "王五", "東莞", "空調", 30))
orderService.Save(NewOrder(4, "張三三", "廣州", "空調", 10))
orderService.Save(NewOrder(5, "李四四", "深圳", "電視", 20))
orderService.Save(NewOrder(6, "王五五", "東莞", "冰箱", 30))
cv := NewCityVisitor()
orderService.Accept(cv)
cv.Report()
pv := NewProductVisitor()
orderService.Accept(pv)
pv.Report()
}
本文的完整源碼,已經同步收錄到我整理的電子教程裏啦,可向我的公衆號「網管叨 bi 叨」發送關鍵字【設計模式】領取。
總結
訪客模式有如下優點
-
解耦了數據結構與數據操作,使得操作集合可以獨立變化。
-
可以通過擴展訪問者角色,實現對數據集的不同操作,程序擴展性更好。
-
元素具體類型並非單一,訪問者均可操作。
-
各角色職責分離,符合單一職責原則。
與此同時它也有以下缺點
-
無法增加元素類型:若系統數據結構對象易於變化,經常有新的數據對象增加進來,則訪問者類必須增加對應元素類型的操作,違背了開閉原則。
-
具體元素變更困難:具體元素增加屬性、刪除屬性等操作, 會導致對應的訪問者類需要進行相應的修改,尤其當有大量訪客類時,修改範圍太大。
-
違背依賴倒置原則:爲了達到 “區別對待”,訪問者角色依賴的是具體元素類型,而不是抽象接口。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qsw89qI8DOXyb4C1XI5QtA