本文主要分析使用访问者模式对生产者模式进行扩展,并分析该模式在kubernetes相关代码的使用场景。
访问者模式
模式介绍
访问者模式(visitor pattern)GoF中表示一个定义如下:作用于某对象结构中的各元素的操作。 它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
以上描述可能较为抽象,我们可以这样理解,面向对象开发的基本原则--开闭原则要求了,当我们设计好一个数据结构或者一段操作代码后,我们需要保留一定的扩展点用于功能的扩展,但不能对原本的功能/结构进行修改。访问者模式就提供了一个合理的扩展的可能性。
访问者模式的UML图如下:
我们需要在Element中定义accept方法,该方法记作为Element的扩展点,其参数为Visitor,扩展的逻辑暴露在Visitor的实现中即可。
代码实现
网上关于访问者模式的代码一般是Java实现,下面笔者使用Golang实现一个简单的访问者模式。
对象结构:
type A struct { ... }
//define accept
func (a A) accept(v Visitor){
v.VisitA(a)
}
定义visitor接口
type Visitor interface {
VisitA(A)
}
实例化visitor, 以下功能为对A进行不同的序列化编码
type JsonVisitor struct {}
func (v JsonVisitor) Visit(a A){
fmt.Printf("%v", json.Marshal(a))
}
type YamlVisitor struct {}
func (v YamlVisitor) Visit(a A){
fmt.Printf("%v", yaml.Marshal(a))
}
Kubernetes原理介绍
本文不是介绍kubernetes的文章,因此本处对kubernetes的基本功能做一个简略的介绍。具体关于kubernetes的内容可以参考官方文档。
kubernetes是一款开源的云计算平台管理软件,其核心在于将包括云计算基础设施(计算Node,计算资源CPU,MEM)、云计算核心概念(部署deployment,服务service,运行实体pod)均抽象为对象,并将对象保存在etcd中。
比如对于无状态任务多实例部署的节点,可以将之抽象问一个replicaset对象,其中包括预期的实例数N,并且于各运行节点实际上报的数量加以比对,来新建实例或删除实例。
因此简单起见kubernetes本质上即为一个分布式对象管理系统,我们可以通过kubectl(官方提供的管理端)创建对象,或者从etcd中获取被管理的对象信息。
Kubectl相关代码分析
如上一节介绍的内容,kubectl是kubernetes官方提供的客户端工具,可以用于创建/查询kubernetes的对象。因此下面我们通过kubectl两个最核心的命令Get/Create来分析其源码设计。
kubectl整体的设计围绕着一个核心的数据结构Info来进行设计,Info的结构如下:
type Info struct {
Client RESTClient
Namespace string
Name string
Source string
Object runtime.Object
//... other things
}
其中最关键的字段为上面的Client(提供kubernetes APIServer REST接口交互),Object(实际对象的内容,Object可以认为是kubernetes中所有被管理对象均需要实现的接口)。
kubernetes的Get方法的实现如下图所示:
以上代码返回一个Result结构,其结构如下:
type Result struct {
err error
visitor Visitor
...
info []*Info
}
func (r *Result) Infos() ([]*Info, error) {
if r.info != nil {
return r.info, nil
}
infos := []*Info{}
err := r.visitor.Visit(func(info *Info, err error) error {
...
infos = append(infos, info)
return nil
})
...
r.info, r.err = infos, err
return infos, err
}
需要重点关注的属性是visitor和info,其中最关键的方法Info() 通过visitor属性构造出Info,并将之返回,因此可以将Result看作是Info的构造器,其构造结果保存在info属性中,构造逻辑保存在visitor中。通过Kubectl Get方法对于Result的使用的相关代码可以印证我们的猜想。
infos, err := r.Infos() //kubectl get.go 505行
//print(infos)
visitor的实现
从上面的分析可以看出,kubectl get的方法最核心的部分在于visitor的使用。通过对代码的分析可以发现visitor的实现使用了我们上面介绍的“访问者模式”。
visitor的定义如下:
type Visitor interface {
Visit(VisitorFunc) error
}
type VisitorFunc func(*Info, error) error
VisitorFunc使用*Info作为参数,因此可以将其作为对Info进行构造以及处理的基本插件,任意一个实现Visit方法的struct均可以作为Result中的Visitor变量。
selector的实现
下面分析Get方法中一个最核心的visitor -- selector。其核心功能为从API server中获取保存的元数据(也就是kubectl get命令的基本功能)。其代码结构如下:
type Selector struct {
Client RESTClient
Namespace string
...
}
//实现visit方法
func (r *Selector) Visit(fn VisitorFunc) error {
... //构造client方法省略
return FollowContinue(&initialOpts, func(options metav1.ListOptions) (runtime.Object, error) {
//从API server中获取list
list, err := helper.List(
r.Namespace,
r.ResourceMapping().GroupVersionKind.GroupVersion().String(),
&options,
)
resourceVersion, _ := metadataAccessor.ResourceVersion(list)
info := &Info{
... //其余参数省略
Object: list,
}
if err := fn(info, nil); err != nil {
return nil, err
}
return list, nil
})
}
selector的visitor主要的实现逻辑即为,对传如的*Info参数进行赋值。
visitor装饰器
visitor本身还可以作为装饰器进行扩展,比如DecoratedVisitor,其中一个属性为Visitor,并且定义了decorators进行扩展。
type DecoratedVisitor struct {
visitor Visitor
decorators []VisitorFunc
}
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
if err != nil {
return err
}
for i := range v.decorators {
if err := v.decorators[i](info, nil); err != nil {
return err
}
}
return fn(info, nil)
})
}
kubectl Create
create代码和get方法类似,通过visitor从本地file中构造Info。
一些思考
以上对kubectl的visitor的分析,其实可以作为一种对生产者模式的可行的扩展,用在一些需要复杂生产逻辑的场景中使用。
比如,我们需要构建一个商品服务,需要从商品库(MySQL)中获取商品信息,且对商品库实现了缓存查询(Redis),如果缓存未命中的情况下才从MySQL中查询。我们可以设计一个ProductGetter的class来实现基本的操作。
type ProductGetter struct{
UseCache bool
}
func (p *ProductGetter) Get() ([]*Product){
var products []*Product
if p.UseCache {
products, err := getFromCache()
}
if products == nil{
products,err = getFromDB()
}
return products
}
以上代码主要有几个问题:
- 将是否用cache包装在Getter之内,以及DB、Cache的查询编排写在生产者中,对于其他的可能的查询方式需要新构建生产者
- 一些通用的操作,比如需要对查询到的product进行校验,新增唯一ID等操作不能很简单的集成在生产者中,需要额外使用其他设计方式进行扩展,且builder代码逻辑会越来越复杂
- error的控制较为复杂,且需要编写在生产者中
仿kubectl写法
实现功能,从Cache中获取info,如果miss则从MySQL中获取,并对查询到的info结构进行name长度判断,以及添加ID的操作。 定义基本的visitor
- CacheVisitor
type CacheVisitor struct {
}
func (v *CacheVisitor) Visit(fn VisitorFunc) error {
fmt.Println("query from Cache")
//return fn(&Info{Name: "Cache info"}, nil)
return fn(nil, errors.New("cache error"))
}
- DBVisitor
type MySQLVisitor struct {
V Visitor
}
func (v *MySQLVisitor) Visit(fn VisitorFunc) error {
return v.V.Visit(func(info *Info, err error) error {
if info == nil {
fmt.Println("query from DB")
info = &Info{Name: "DB info, yes ok"}
}
return fn(info, nil)
})
}
- DecoratedVisitor
type EagerVisitorList []Visitor
// Visit implements Visitor, and gathers errors that occur during processing until
// all sub visitors have been visited.
func (l EagerVisitorList) Visit(fn VisitorFunc) error {
var errs []error
for i := range l {
err := l[i].Visit(func(info *Info, err error) error {
if err != nil {
errs = append(errs, err)
return nil
}
if err := fn(info, nil); err != nil {
errs = append(errs, err)
}
return nil
})
if err != nil {
errs = append(errs, err)
}
}
return errors.New("error1")
}
- 定义validator实现验证以及ID添加操作(实际操作可以拆成两个func实现)
var validator = func(info *Info, err error) error {
if info == nil {
panic("no info")
}
if len(info.Name) < 10 {
panic("short name")
}
if info.Id == 0 {
info.Id = 1
}
return nil
}
执行:
func main() {
var v Visitor
v = &CacheVisitor{}
v = &MySQLVisitor{V: v}
v1 := &DecoratedVisitor{
V: v,
decorators: []VisitorFunc{
validator,
},
}
v = EagerVisitorList([]Visitor{v1})
v.Visit(func(info *Info, err error) error {
fmt.Println(info)
return nil
})
}