上一篇:【Go完成】实践GoF的23种规划形式:迭代器形式

简单的分布式运用体系(示例代码工程):github.com/ruanrunxue/…

简介

GoF 对拜访者形式Visitor Pattern)的界说如下:

Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

拜访者形式的意图是,解耦数据结构和算法,使得体系能够在不改变现有代码结构的基础上,为目标新增一种新的操作。

上一篇介绍的 迭代器形式 也做到了数据结构和算法的解耦,不过它专心于遍历算法。拜访者形式,则在遍历的一同,将操作作用到数据结构上,一个常见的运用场景是语法树的解析。

UML 结构

【Go实现】实践GoF的23种设计模式:访问者模式

场景上下文

在 简单的分布式运用体系(示例代码工程)中,db 模块用来存储服务注册和监控信息,它是一个 key-value 数据库。另外,咱们给 db 模块笼统出 Table 目标:

 // demo/db/table.go
 package db
 ​
 // Table 数据表界说
 type Table struct {
     name            string
     metadata        map[string]int // key为特点名,value特点值的索引, 对应到record上存储
     records         map[interface{}]record
     iteratorFactory TableIteratorFactory // 默许运用随机迭代器
 }

意图是供给类似于联系型数据库的按列查询能力,比方:

【Go实现】实践GoF的23种设计模式:访问者模式

上述的按列查询仅仅等值比较,未来还可能会完成正则表达式匹配等办法,因而咱们需要规划出可供未来扩展的接口。这种场景,运用拜访者形式正合适。

代码完成

 // demo/db/table_visitor.go
 package db
 ​
 // 要害点1: 界说表查询的拜访者笼统接口,答应后续扩展查询办法
 type TableVisitor interface {
     // 要害点2: Visit办法以Element作为入参,这里的Element为Table目标
     Visit(table *Table) ([]interface{}, error)
 }
 ​
 // 要害点3: 界说Visitor笼统接口的完成目标,这里FieldEqVisitor完成按列等值查询逻辑
 type FieldEqVisitor struct {
     field string
     value interface{}
 }
 ​
 // 要害点4: 为FieldEqVisitor界说Visit办法,完成详细的等值查询逻辑
 func (f *FieldEqVisitor) Visit(table *Table) ([]interface{}, error) {
     result := make([]interface{}, 0)
     idx, ok := table.metadata[f.field]
     if !ok {
         return nil, ErrRecordNotFound
    }
     for _, r := range table.records {
         if reflect.DeepEqual(r.values[idx], f.value) {
             result = append(result, r)
        }
    }
     if len(result) == 0 {
         return nil, ErrRecordNotFound
    }
     return result, nil
 }
 ​
 func NewFieldEqVisitor(field string, value interface{}) *FieldEqVisitor {
     return &FieldEqVisitor{
         field: field,
         value: value,
    }
 }
 ​
 // demo/db/table.go
 package db
 ​
 type Table struct {...}
 // 要害点5: 为Element界说Accept办法,入参为Visitor接口
 func (t *Table) Accept(visitor TableVisitor) ([]interface{}, error) {
     return visitor.Visit(t)
 }

客户端能够这么运用:

 func client() {
     table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion)))
     table.Insert(1, &testRegion{Id: 1, Name: "beijing"})
     table.Insert(2, &testRegion{Id: 2, Name: "beijing"})
     table.Insert(3, &testRegion{Id: 3, Name: "guangdong"})
 ​
   visitor := NewFieldEqVisitor("name", "beijing")
     result, err := table.Accept(visitor)
     if err != nil {
         t.Error(err)
    }
     if len(result) != 2 {
         t.Errorf("visit failed, want 2, got %d", len(result))
    }
 }

总结完成拜访者形式的几个要害点:

  1. 界说拜访者笼统接口,上述比方为 TableVisitor, 意图是答应后续扩展表查询办法。

  2. 拜访者笼统接口中,Visit 办法以 Element 作为入参,上述比方中, Element 为 Table 目标。

  3. 为 Visitor 笼统接口界说详细的完成目标,上述比方为 FieldEqVisitor

  4. 在拜访者的 Visit 办法中完成详细的业务逻辑,上述比方中 FieldEqVisitor.Visit(...) 完成了按列等值查询逻辑。

  5. 在被拜访者 Element 中界说 Accept 办法,以拜访者 Visitor 作为入参。上述比方中为 Table.Accept(...) 办法。

扩展

Go 风格完成

上述完成是典型的面向目标风格,下面以 Go 风格重新完成拜访者形式:

 // demo/db/table_visitor_func.go
 package db
 ​
 // 要害点1: 界说一个拜访者函数类型
 type TableVisitorFunc func(table *Table) ([]interface{}, error)
 ​
 // 要害点2: 界说工厂办法,工厂办法回来的是一个拜访者函数,完成了详细的拜访逻辑
 func NewFieldEqVisitorFunc(field string, value interface{}) TableVisitorFunc {
     return func(table *Table) ([]interface{}, error) {
         result := make([]interface{}, 0)
         idx, ok := table.metadata[field]
         if !ok {
             return nil, ErrRecordNotFound
        }
         for _, r := range table.records {
             if reflect.DeepEqual(r.values[idx], value) {
                 result = append(result, r)
            }
        }
         if len(result) == 0 {
             return nil, ErrRecordNotFound
        }
         return result, nil
    }
 }
 ​
 // 要害点3: 为Element界说Accept办法,入参为Visitor函数类型
 func (t *Table) AcceptFunc(visitorFunc TableVisitorFunc) ([]interface{}, error) {
     return visitorFunc(t)
 }

客户端能够这么运用:

 func client() {
     table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion)))
     table.Insert(1, &testRegion{Id: 1, Name: "beijing"})
     table.Insert(2, &testRegion{Id: 2, Name: "beijing"})
     table.Insert(3, &testRegion{Id: 3, Name: "guangdong"})
 ​
     result, err := table.AcceptFunc(NewFieldEqVisitorFunc("name", "beijing"))
     if err != nil {
         t.Error(err)
    }
     if len(result) != 2 {
         t.Errorf("visit failed, want 2, got %d", len(result))
    }
 }

Go 风格的完成,利用了函数闭包的特点,更加简洁了。

总结几个完成要害点:

  1. 界说一个拜访者函数类型,函数签名以 Element 作为入参,上述比方为 TableVisitorFunc 类型。

  2. 界说一个工厂办法,工厂办法回来的是详细的拜访拜访者函数,上述比方为 NewFieldEqVisitorFunc 办法。这里利用了函数闭包的特性,在拜访者函数中直接引用工厂办法的入参,与 FieldEqVisitor 中持有两个成员特点的作用一样。

  3. 为 Element 界说 Accept 办法,入参为 Visitor 函数类型 ,上述比方是 Table.AcceptFunc(...) 办法。

与迭代器形式结合

拜访者形式常常与迭代器形式一同运用。比方上述比方中,假如你界说的 Visitor 完成不在 db 包内,那么就无法直接拜访 Table 的数据,这时就需要经过 Table 供给的迭代器来完成。

在 简单的分布式运用体系(示例代码工程)中,db 模块存储的服务注册信息如下:

 // demo/service/registry/model/service_profile.go
 package model
 ​
 // ServiceProfileRecord 存储在数据库里的类型
 type ServiceProfileRecord struct {
     Id       string        // 服务ID
     Type     ServiceType   // 服务类型
     Status   ServiceStatus // 服务状态
     Ip       string        // 服务IP
     Port     int           // 服务端口
     RegionId string        // 服务所属regionId
     Priority int           // 服务优先级,规模0~100,值越低,优先级越高
     Load     int           // 服务负载,负载越高表明服务处理的业务压力越大
 }

现在,咱们要查询契合指定 ServiceIdServiceType 的服务记载,能够这么完成一个 Visitor:

 // demo/service/registry/model/service_profile.go
 package model
 ​
 type ServiceProfileVisitor struct {
     svcId   string
     svcType ServiceType
 }
 ​
 func (s *ServiceProfileVisitor) Visit(table *db.Table) ([]interface{}, error) {
     var result []interface{}
   // 经过迭代器来遍历Table的所有数据
     iter := table.Iterator()
     for iter.HasNext() {
         profile := new(ServiceProfileRecord)
         if err := iter.Next(profile); err != nil {
             return nil, err
        }
         // 先匹配ServiceId,假如一致则无须匹配ServiceType
         if profile.Id != "" && profile.Id == s.svcId {
             result = append(result, profile)
             continue
        }
         // ServiceId匹配不上,再匹配ServiceType
         if profile.Type != "" && profile.Type == s.svcType {
             result = append(result, profile)
        }
    }
     return result, nil
 }

典型运用场景

  • k8s 中,kubectl 经过拜访者形式来处理用户界说的各类资源。

  • 编译器中,一般运用拜访者形式来完成对语法树解析,比方 LLVM。

  • 希望对一个复杂的数据结构履行某些操作,并支撑后续扩展。

优缺陷

优点

  • 数据结构和操作算法解耦,契合 单一职责准则。

  • 支撑对数据结构扩展多种操作,具备较强的可扩展性,契合 开闭准则。

缺陷

  • 拜访者形式某种程度上,要求数据结构有必要对外露出其内在完成,否则拜访者就无法遍历其间数据(能够结合迭代器形式来处理该问题)。

  • 假如被拜访目标内的数据结构改变,可能要更新所有的拜访者完成。

与其他形式的关联

  • 拜访者形式 常常和 迭代器形式 一同运用,使得被拜访目标无须向外露出内在数据结构。

  • 也常常和 组合形式 一同运用,比方在语法树解析中,递归拜访和解析树的每个节点(节点组合成树)。

文章配图

能够在 用Keynote画出手绘风格的配图 中找到文章的绘图办法。

参阅

[1] 【Go完成】实践GoF的23种规划形式:SOLID准则, 元闰子

[2] 【Go完成】实践GoF的23种规划形式:迭代器形式, 元闰子

[3] Design Patterns, Chapter 5. Behavioral Patterns, GoF

[4] GO 编程形式:K8S VISITOR 形式, 酷壳

[5] 拜访者形式, refactoringguru.cn

更多文章请重视微信大众号:元闰子的邀请