Go 編程模式:k8s Visitor 模式

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

本文是全系列中第9 / 9篇:Go編程模式

« 上一篇文章

一個簡單示例

我們還是先來看一個簡單設計模式的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序列化的

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() 方法時,其調用了自己結構體內的那個 VisitorVisitor() 方法,這其實是一種修飾器的模式,用另一個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 functionLogVisitor() 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微信公衆賬號和微信小程序

(轉載本站文章請註明作者和出處 酷 殼 – CoolShell ,請勿用於任何商業用途)

——=== 訪問 酷殼404頁面 尋找遺失兒童。 ===——
好爛啊 有點差 湊合看看 還不錯 很精彩 (沒人打分)

Loading...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章