likes
comments
collection
share

用访客模式解耦数据和视图, 业务代码也能优雅起来!

作者站长头像
站长
· 阅读数 20

大家好,这里是每周都在陪你一起进步的网管~!今天继续学习设计模式—访客模式

访客模式也叫访问者模式(Visitor Pattern)是一种将数据结构对象与数据操作分离的设计模式,可以在不改变数据结构对象类结构的前提下定义作用于这些对象的新的操作, 属于行为型设计模式。

访问者模式主要适用于以下应用场景:

  1. 数据结构稳定,作用于数据结构的操作经常变化的场景。
  2. 需要数据结构与数据操作分离的场景。
  3. 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。

访客模式怎么工作?

访问者模式通过将算法与对象结构分离来工作,这里说的算法指的是对对象的操作。为此,我们需要定义了一个表示算法的接口--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叨」发送关键字【设计模式】领取。

用访客模式解耦数据和视图, 业务代码也能优雅起来!

总结

访客模式有如下优点

  • 解耦了数据结构与数据操作,使得操作集合可以独立变化。
  • 可以通过扩展访问者角色,实现对数据集的不同操作,程序扩展性更好。
  • 元素具体类型并非单一,访问者均可操作。
  • 各角色职责分离,符合单一职责原则。

与此同时它也有以下缺点

  • 无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则。
  • 具体元素变更困难:具体元素增加属性、删除属性等操作, 会导致对应的访问者类需要进行相应的修改,尤其当有大量访客类时,修改范围太大。
  • 违背依赖倒置原则:为了达到“区别对待”,访问者角色依赖的是具体元素类型,而不是抽象接口。