本篇文章主要想討論一下,Kubernetes 的 kubectl
命令中的使用到到的一個編程模式 – Visitor(注:其實,kubectl
主要使用到了兩個一個是Builder,另一個是Visitor)。本來,Visitor 是面向對象設計模英中一個很重要的設計模款(參看Wikipedia Visitor Pattern詞條),這個模式是一種將算法與操作對象的結構分離的一種方法。這種分離的實際結果是能夠在不修改結構的情況下向現有對象結構添加新操作,是遵循開放/封閉原則的一種方法。這篇文章我們重點看一下 kubelet
中是怎麼使用函數式的方法來實現這個模式的。
本文是全系列中第9 / 9篇:Go編程模式
- Go編程模式:切片,接口,時間和性能
- Go 編程模式:錯誤處理
- Go 編程模式:Functional Options
- Go編程模式:委託和反轉控制
- Go編程模式:Map-Reduce
- Go 編程模式:Go Generation
- Go編程模式:修飾器
- Go編程模式:Pipeline
- Go 編程模式:k8s Visitor 模式
一個簡單示例
我們還是先來看一個簡單設計模式的Visitor的示例。
- 我們的代碼中有一個
Visitor
的函數定義,還有一個Shape
接口,其需要使用Visitor
函數做爲參數。 - 我們的實例的對象
Circle
和Rectangle
實現了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
結構中的 Name
和 NameSpace
成員
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 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 ,請勿用於任何商業用途)