訪客模式也叫訪問者模式(Visitor Pattern)是一種將數(shù)據(jù)結(jié)構(gòu)對(duì)象與數(shù)據(jù)操作分離的設(shè)計(jì)模式,可以在不改變數(shù)據(jù)結(jié)構(gòu)對(duì)象類結(jié)構(gòu)的前提下定義作用于這些對(duì)象的新的操作, 屬于行為型設(shè)計(jì)模式。
大家好,這里是每周都在陪你一起進(jìn)步的網(wǎng)管~!今天繼續(xù)學(xué)習(xí)設(shè)計(jì)模式—訪客模式
訪客模式也叫訪問者模式(Visitor Pattern)是一種將數(shù)據(jù)結(jié)構(gòu)對(duì)象與數(shù)據(jù)操作分離的設(shè)計(jì)模式,可以在不改變數(shù)據(jù)結(jié)構(gòu)對(duì)象類結(jié)構(gòu)的前提下定義作用于這些對(duì)象的新的操作, 屬于行為型設(shè)計(jì)模式。
訪問者模式主要適用于以下應(yīng)用場(chǎng)景:
- 數(shù)據(jù)結(jié)構(gòu)穩(wěn)定,作用于數(shù)據(jù)結(jié)構(gòu)的操作經(jīng)常變化的場(chǎng)景。
- 需要數(shù)據(jù)結(jié)構(gòu)與數(shù)據(jù)操作分離的場(chǎng)景。
- 需要對(duì)不同數(shù)據(jù)類型(元素)進(jìn)行操作,而不使用分支判斷具體類型的場(chǎng)景。
訪客模式怎么工作?
訪問者模式通過將算法與對(duì)象結(jié)構(gòu)分離來工作,這里說的算法指的是對(duì)對(duì)象的操作。為此,我們需要定義了一個(gè)表示算法的接口--Visitor。該接口將為對(duì)象結(jié)構(gòu)中的每個(gè)類(一般稱為元素類)提供一個(gè)方法。每個(gè)方法都將元素類的一個(gè)實(shí)例作為參數(shù)。表示對(duì)象結(jié)構(gòu)的所有元素類也會(huì)實(shí)現(xiàn)一個(gè)Element接口,該接口定義了接受訪問者的方法Accpet。此方法將訪問者接口的實(shí)現(xiàn)作為參數(shù)。當(dāng)Accpet方法被調(diào)用時(shí),訪問者實(shí)例對(duì)應(yīng)的方法就會(huì)被調(diào)用,通過訪問者完成對(duì)元素類實(shí)例的操作。
下面我們看一下訪問者模式的類結(jié)構(gòu)。
訪客模式結(jié)構(gòu)
訪問者的類結(jié)構(gòu)可以用下面的UML類圖來表示:

- 訪客接口 (Visitor) 聲明了一系列以表示對(duì)象結(jié)構(gòu)的具體元素為參數(shù)的訪問者方法。 如果編程語言支持重載, 這些方法的名稱可以是相同的, 但是其參數(shù)一定是不同的。
- 具體訪客 (Concrete Visitor) 會(huì)為不同的具體元素類實(shí)現(xiàn)相同行為的幾個(gè)不同版本。
- 元素 (Element) 接口,聲明了一個(gè)方法來 “接收” 訪問者。 該方法必須有一個(gè)參數(shù)被聲明為訪問者接口類型。
- 具體元素 (Concrete Element) 必須實(shí)現(xiàn)接收方法。 該方法的目的是根據(jù)當(dāng)前元素類將其調(diào)用重定向到相應(yīng)訪問者的方法。 請(qǐng)注意, 即使元素基類實(shí)現(xiàn)了該方法, 所有子類都必須對(duì)其進(jìn)行重寫并調(diào)用訪客對(duì)象中的合適方法。
訪客模式代碼示例
在這個(gè)用訪客模式實(shí)現(xiàn)不同維度的訂單統(tǒng)計(jì)的例子里,假設(shè)我們建設(shè)了一個(gè)訂單管理系統(tǒng), 現(xiàn)在系統(tǒng)中要求能按照不同維度統(tǒng)計(jì)分析銷售訂單
- 區(qū)域銷售報(bào)表: 需按銷售區(qū)域, 統(tǒng)計(jì)銷售情況
- 品類銷售報(bào)表: 需根據(jù)不同產(chǎn)品, 統(tǒng)計(jì)銷售情況
以后還有可能增加其他維度的銷售統(tǒng)計(jì)報(bào)表,針對(duì)這個(gè)需求我們可以根據(jù)訪問者模式, 可將不同的報(bào)表, 設(shè)計(jì)為訂單的訪問者。 首先定義訂單實(shí)體和它要實(shí)現(xiàn)的Element接口
"本文使用的完整可運(yùn)行源碼
去公眾號(hào)「網(wǎng)管叨bi叨」發(fā)送【設(shè)計(jì)模式】即可領(lǐng)取"
// 訂單服務(wù)接口
type IOrderService interface {
Save(order *Order) error
// 有的教程里把接收 visitor 實(shí)現(xiàn)的方法名定義成 Accept
Accept(visitor IOrderVisitor)
}
// 訂單實(shí)體類,實(shí)現(xiàn)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,
}
}
接下來定義生成各種銷售報(bào)表的訪客類,以及它們實(shí)現(xiàn)的訪客接口
"本文使用的完整可運(yùn)行源碼
去公眾號(hào)「網(wǎng)管叨bi叨」發(fā)送【設(shè)計(jì)模式】即可領(lǐng)取"
type IOrderVisitor interface {
// 這里參數(shù)不能定義成 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),
}
}
// 品類銷售報(bào)表, 按產(chǎn)品匯總銷售情況, 實(shí)現(xiàn)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生成各種銷售報(bào)表
func main() {
orderService := NewOrderService()
orderService.Save(NewOrder(1, "張三", "廣州", "電視", 10))
orderService.Save(NewOrder(2, "李四", "深圳", "冰箱", 20))
orderService.Save(NewOrder(3, "王五", "東莞", "空調(diào)", 30))
orderService.Save(NewOrder(4, "張三三", "廣州", "空調(diào)", 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()
}