九、GO 程式設計模式:K8S VISITOR 模式

zhaocrazy發表於2022-02-08

本篇文章主要想討論一下,Kubernetes 的 kubectl 命令中的使用到到的一個程式設計模式 – Visitor(注:其實,kubectl 主要使用到了兩個一個是Builder,另一個是Visitor)。本來,Visitor 是物件導向設計模英中一個很重要的設計模款(參看Wikipedia Visitor Pattern詞條),這個模式是一種將演算法與操作物件的結構分離的一種方法。這種分離的實際結果是能夠在不修改結構的情況下向現有物件結構新增新操作,是遵循開放/封閉原則的一種方法。這篇文章我們重點看一下 kubelet 中是怎麼使用函式式的方法來實現這個模式的。

一個簡單示例

我們還是先來看一個簡單設計模式的Visitor的示例。

  • 我們的程式碼中有一個Visitor的函式定義,還有一個Shape介面,其需要使用 Visitor函式做為引數。
  • 我們的例項的物件 CircleRectangle實現了 Shape 的介面的 accept() 方法,這個方法就是等外面給我傳遞一個Visitor。
    package main
    

import (
“encoding/json”
“encoding/xml”
“fmt”
)

type Visitor func(shape Shape)

type Shape interface {
accept(Visitor)
}

type Circle struct {
Radius int
}

func (c Circle) accept(v Visitor) {
v(c)
}

type Rectangle struct {
Width, Heigh int
}

func (r Rectangle) accept(v Visitor) {
v(r)
}

然後,我們實現兩個Visitor,一個是用來做JSON序列化的,另一個是用來做XML序列化的
```go
func JsonVisitor(shape Shape) {
    bytes, err := json.Marshal(shape)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bytes))
}

func XmlVisitor(shape Shape) {
    bytes, err := xml.Marshal(shape)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bytes))
}

下面是我們的使用Visitor這個模式的程式碼

func main() {
  c := Circle{10}
  r :=  Rectangle{100, 200}
  shapes := []Shape{c, r}

  for _, s := range shapes {
    s.accept(JsonVisitor)
    s.accept(XmlVisitor)
  }

}

其實,這段程式碼的目的就是想解耦 資料結構和 演算法,使用 Strategy 模式也是可以完成的,而且會比較乾淨。但是在有些情況下,多個Visitor是來訪問一個資料結構的不同部分,這種情況下,資料結構有點像一個資料庫,而各個Visitor會成為一個個小應用。 kubectl就是這種情況。

k8s相關背景

接下來,我們再來了解一下相關的知識背景:

  • 對於Kubernetes,其抽象了很多種的Resource,比如:Pod, ReplicaSet, ConfigMap, Volumes, Namespace, Roles …. 種類非常繁多,這些東西構成為了Kubernetes的資料模型(點選 Kubernetes Resources 地圖 檢視其有多複雜)
  • kubectl 是Kubernetes中的一個客戶端命令,操作人員用這個命令來操作Kubernetes。kubectl 會聯絡到 Kubernetes 的API Server,API Server會聯絡每個節點上的 kubelet ,從而達到控制每個結點。
  • kubectl 主要的工作是處理使用者提交的東西(包括,命令列引數,yaml檔案等),然後其會把使用者提交的這些東西組織成一個資料結構體,然後把其傳送給 API Server。
  • 相關的原始碼在 src/k8s.io/cli-runtime/pkg/resource/visitor.go 中(原始碼連結

kubectl 的程式碼比較複雜,不過,其本原理簡單來說,它從命令列和yaml檔案中獲取資訊,通過Builder模式並把其轉成一系列的資源,最後用 Visitor 模式模式來迭代處理這些Reources

下面我們來看看 kubectl 的實現,為了簡化,我用一個小的示例來表明 ,而不是直接分析複雜的原始碼。

kubectl的實現方法

Visitor模式定義

首先,kubectl 主要是用來處理 Info結構體,下面是相關的定義:

type VisitorFunc func(*Info, error) error

type Visitor interface {
    Visit(VisitorFunc) error
}

type Info struct {
    Namespace   string
    Name        string
    OtherThings string
}
func (info *Info) Visit(fn VisitorFunc) error {
  return fn(info, nil)
}

我們可以看到,

  • 有一個 VisitorFunc 的函式型別的定義
  • 一個 Visitor 的介面,其中需要 Visit(VisitorFunc) error 的方法(這就像是我們上面那個例子的 Shape )
  • 最後,為Info 實現 Visitor 介面中的 Visit() 方法,實現就是直接呼叫傳進來的方法(與前面的例子相仿)
    我們再來定義幾種不同型別的 Visitor。
    Name Visitor

這個Visitor 主要是用來訪問 Info 結構中的 NameNameSpace 成員

type NameVisitor struct {
  visitor Visitor
}

func (v NameVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    fmt.Println("NameVisitor() before call function")
    err = fn(info, err)
    if err == nil {
      fmt.Printf("==> Name=%s, NameSpace=%s\n", info.Name, info.Namespace)
    }
    fmt.Println("NameVisitor() after call function")
    return err
  })
}

我們可以看到,上面的程式碼:

  • 宣告瞭一個 NameVisitor 的結構體,這個結構體裡有一個 Visitor 介面成員,這裡意味著多型。
  • 在實現 Visit() 方法時,其呼叫了自己結構體內的那個 Visitor的 Visitor() 方法,這其實是一種修飾器的模式,用另一個Visitor修飾了自己(關於修飾器模式,參看《Go程式設計模式:修飾器》)
Other Visitor

這個Visitor主要用來訪問 Info 結構中的 OtherThings 成員

type OtherThingsVisitor struct {
  visitor Visitor
}

func (v OtherThingsVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    fmt.Println("OtherThingsVisitor() before call function")
    err = fn(info, err)
    if err == nil {
      fmt.Printf("==> OtherThings=%s\n", info.OtherThings)
    }
    fmt.Println("OtherThingsVisitor() after call function")
    return err
  })
}

實現邏輯同上,我就不再重新講了

Log Visitor

type LogVisitor struct {
  visitor Visitor
}

func (v LogVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    fmt.Println("LogVisitor() before call function")
    err = fn(info, err)
    fmt.Println("LogVisitor() after call function")
    return err
  })
}
使用方程式碼

現在我們看看如果使用上面的程式碼:

func main() {
  info := Info{}
  var v Visitor = &info
  v = LogVisitor{v}
  v = NameVisitor{v}
  v = OtherThingsVisitor{v}

  loadFile := func(info *Info, err error) error {
    info.Name = "Hao Chen"
    info.Namespace = "MegaEase"
    info.OtherThings = "We are running as remote team."
    return nil
  }
  v.Visit(loadFile)
}

上面的程式碼,我們可以看到

  • Visitor們一層套一層
  • 我用 loadFile 假裝從檔案中讀如資料
  • 最後一條 v.Visit(loadfile) 我們上面的程式碼就全部開始啟用工作了。

上面的程式碼輸出如下的資訊,你可以看到程式碼的執行順序是怎麼執行起來了

LogVisitor() before call function
NameVisitor() before call function
OtherThingsVisitor() before call function
==> OtherThings=We are running as remote team.
OtherThingsVisitor() after call function
==> Name=Hao Chen, NameSpace=MegaEase
NameVisitor() after call function
LogVisitor() after call function

我們可以看到,上面的程式碼有以下幾種功效:

解耦了資料和程式。
使用了修飾器模式
還做出來pipeline的模式
所以,其實,我們是可以把上面的程式碼重構一下的。

Visitor修飾器

下面,我們用修飾器模式來重構一下上面的程式碼。

type DecoratedVisitor struct {
  visitor    Visitor
  decorators []VisitorFunc
}

func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor {
  if len(fn) == 0 {
    return v
  }
  return DecoratedVisitor{v, fn}
}

// Visit implements Visitor
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    if err != nil {
      return err
    }
    if err := fn(info, nil); err != nil {
      return err
    }
    for i := range v.decorators {
      if err := v.decorators[i](info, nil); err != nil {
        return err
      }
    }
    return nil
  })
}

上面的程式碼並不複雜,

  • 用一個 DecoratedVisitor 的結構來存放所有的VistorFunc函式
  • NewDecoratedVisitor 可以把所有的 VisitorFunc轉給它,構造 DecoratedVisitor 物件。
  • DecoratedVisitor實現了 Visit() 方法,裡面就是來做一個for-loop,順著呼叫所有的 VisitorFunc
    於是,我們的程式碼就可以這樣運作了:
    info := Info{}
    var v Visitor = &info
    v = NewDecoratedVisitor(v, NameVisitor, OtherVisitor)
    

v.Visit(LoadFile)
```
是不是比之前的那個簡單?注意,這個DecoratedVisitor 同樣可以成為一個Visitor來使用。

好,上面的這些程式碼全部存在於 kubectl 的程式碼中,你看懂了這裡面的程式碼邏輯,相信你也能夠看懂 kubectl 的程式碼了。
(全文完)本文非本人所作,轉載左耳朵耗子部落格和出處 酷 殼 – CoolShell

本作品採用《CC 協議》,轉載必須註明作者和本文連結
滴水穿石,石破天驚----馬乂

相關文章