golang設計模式-以kubernetes原始碼為例

雨落雲風發表於2017-11-20

概述

design pattern 介紹設計模式
engineer pattern 總結先進的工程模式

design pattern

參考

simplefactory

對golang來說就是Newxx函式,返回interface, kubernetes interface隨處可見,可以說能用interface抽象的就是interface,隨便舉一個例子

// k8s.io/kubernetes/vendor/k8s.io/client-go/tools/cache/store.go
func NewStore(keyFunc KeyFunc) Store {
    return &cache{
        cacheStorage: NewThreadSafeStore(Indexers{}, Indices{}),
        keyFunc:      keyFunc,
    }
}

type cache struct {
    // cacheStorage bears the burden of thread safety for the cache
    cacheStorage ThreadSafeStore
    // keyFunc is used to make the key for objects stored in and retrieved from items, and
    // should be deterministic.
    keyFunc KeyFunc
}

type Store interface {
    Add(obj interface{}) error
    Update(obj interface{}) error
    Delete(obj interface{}) error
    List() []interface{}
    ListKeys() []string
    Get(obj interface{}) (item interface{}, exists bool, err error)
    GetByKey(key string) (item interface{}, exists bool, err error)

    // Replace will delete the contents of the store, using instead the
    // given list. Store takes ownership of the list, you should not reference
    // it after calling this function.
    Replace([]interface{}, string) error
    Resync() error
}複製程式碼

facade / adapter / decorator / delegate / bridge / mediator / composite

組合模式的不同形式,這些隨處可見,也不用深究其中的區別。

singleton

kubernetes/golang 用得很少,一般使用全域性變數(如配置) (比如 net/http package 的 http.DefaultClient 和 http.DefaultServeMux). 或者作為context傳遞。
實現方式可以參考marcio.io/2015/07/sin…

比較常見的一種方式是double check

func GetInstance() *singleton {
    if instance == nil {     // <-- Not yet perfect. since it's not fully atomic
        mu.Lock()
        defer mu.Unlock()

        if instance == nil {
            instance = &singleton{}
        }
    }
    return instance
}複製程式碼

但是在golang裡面有更好的一種方式,用"Once"

type singleton struct {
}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}複製程式碼

factory/ abstract factory / builder

關於這幾種creational patterns 的區別:

  • Builder focuses on constructing a complex object step by step. Abstract Factory emphasizes a family of product objects (either simple or complex). Builder returns the product as a final step, but as far as the Abstract Factory is concerned, the product gets returned immediately.
  • Builder often builds a Composite.
  • Often, designs start out using Factory Method (less complicated, more customizable, subclasses proliferate) and evolve toward Abstract Factory, Prototype, or Builder (more flexible, more complex) as the designer discovers where more flexibility is needed.
  • Sometimes creational patterns are complementary: Builder can use one of the other patterns to implement which components get built. Abstract Factory, Builder, and Prototype can use Singleton in their implementations.

factory

// k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/runtime/serializer/codec_factory.go
func NewCodecFactory(scheme *runtime.Scheme) CodecFactory {
    serializers := newSerializersForScheme(scheme, json.DefaultMetaFactory)
    return newCodecFactory(scheme, serializers)
}

func (f CodecFactory) LegacyCodec(version ...schema.GroupVersion) runtime.Codec {
    return versioning.NewDefaultingCodecForScheme(f.scheme, f.legacySerializer, f.universal, schema.GroupVersions(version), runtime.InternalGroupVersioner)
}複製程式碼

abstract factory 以SharedInformerFactory為例,這個factory 能夠create app/core/batch ...等各種interface

//k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion/factory.go
func NewSharedInformerFactory(client internalclientset.Interface, defaultResync time.Duration) SharedInformerFactory {
    return &sharedInformerFactory{
        client:           client,
        defaultResync:    defaultResync,
        informers:        make(map[reflect.Type]cache.SharedIndexInformer),
        startedInformers: make(map[reflect.Type]bool),
    }
}

// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
type SharedInformerFactory interface {
    internalinterfaces.SharedInformerFactory
    ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
    WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool

    Admissionregistration() admissionregistration.Interface
    Apps() apps.Interface
    Autoscaling() autoscaling.Interface
    Batch() batch.Interface
    Certificates() certificates.Interface
    Core() core.Interface
    Extensions() extensions.Interface
    Networking() networking.Interface
    Policy() policy.Interface
    Rbac() rbac.Interface
    Scheduling() scheduling.Interface
    Settings() settings.Interface
    Storage() storage.Interface
}

// sharedInformerFactory 是具體的stuct
func (f *sharedInformerFactory) Apps() apps.Interface {
    return apps.New(f)
}複製程式碼

builder

// k8s.io/kubernetes/pkg/controller/client_builder.go

func NewForConfigOrDie(c *rest.Config) *Clientset {
    var cs Clientset
    cs.admissionregistrationV1alpha1 = admissionregistrationv1alpha1.NewForConfigOrDie(c)
    cs.appsV1beta1 = appsv1beta1.NewForConfigOrDie(c)
    cs.appsV1beta2 = appsv1beta2.NewForConfigOrDie(c)
    cs.appsV1 = appsv1.NewForConfigOrDie(c)
    cs.authenticationV1 = authenticationv1.NewForConfigOrDie(c)
    cs.authenticationV1beta1 = authenticationv1beta1.NewForConfigOrDie(c)
    cs.authorizationV1 = authorizationv1.NewForConfigOrDie(c)
    cs.authorizationV1beta1 = authorizationv1beta1.NewForConfigOrDie(c)
    cs.autoscalingV1 = autoscalingv1.NewForConfigOrDie(c)
    cs.autoscalingV2beta1 = autoscalingv2beta1.NewForConfigOrDie(c)
    cs.batchV1 = batchv1.NewForConfigOrDie(c)
    cs.batchV1beta1 = batchv1beta1.NewForConfigOrDie(c)
    cs.batchV2alpha1 = batchv2alpha1.NewForConfigOrDie(c)
    cs.certificatesV1beta1 = certificatesv1beta1.NewForConfigOrDie(c)
    cs.coreV1 = corev1.NewForConfigOrDie(c)
    cs.extensionsV1beta1 = extensionsv1beta1.NewForConfigOrDie(c)
    cs.networkingV1 = networkingv1.NewForConfigOrDie(c)
    cs.policyV1beta1 = policyv1beta1.NewForConfigOrDie(c)
    cs.rbacV1 = rbacv1.NewForConfigOrDie(c)
    cs.rbacV1beta1 = rbacv1beta1.NewForConfigOrDie(c)
    cs.rbacV1alpha1 = rbacv1alpha1.NewForConfigOrDie(c)
    cs.schedulingV1alpha1 = schedulingv1alpha1.NewForConfigOrDie(c)
    cs.settingsV1alpha1 = settingsv1alpha1.NewForConfigOrDie(c)
    cs.storageV1beta1 = storagev1beta1.NewForConfigOrDie(c)
    cs.storageV1 = storagev1.NewForConfigOrDie(c)

    cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
    return &cs
}複製程式碼

prototype

原型模式是建立型模式的一種,其特點在於通過“複製”一個已經存在的例項來返回新的例項,而不是新建例項。被複制的例項就是我們所稱的“原型”,這個原型是可定製的。
The Prototype Pattern creates duplicate objects while keeping performance in mind. It's a part of the creational patterns and provides one of the best ways to create an object.

參考blog.ralch.com/tutorial/de…

kubernetes使用了deepcopy-gen自動生成物件的deepcopy等方法,比如下面的一個生成的例子

// k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/apis/audit/zz_generated.deepcopy.go
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GroupResources) DeepCopyInto(out *GroupResources) {
    *out = *in
    if in.Resources != nil {
        in, out := &in.Resources, &out.Resources
        *out = make([]string, len(*in))
        copy(*out, *in)
    }
    if in.ResourceNames != nil {
        in, out := &in.ResourceNames, &out.ResourceNames
        *out = make([]string, len(*in))
        copy(*out, *in)
    }
    return
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupResources.
func (in *GroupResources) DeepCopy() *GroupResources {
    if in == nil {
        return nil
    }
    out := new(GroupResources)
    in.DeepCopyInto(out)
    return out
}複製程式碼

生成的工具在這裡 k8s.io/kubernetes/vendor/k8s.io/code-generator/cmd/deepcopy-gen/main.go
使用 github.com/kubernetes/…

observer

這個模式在kubernetes裡面也比較常見,比如sharedInformer就是一個觀察者模式的實現

// k8s.io/client-go/tools/cache/shared_informer.go
func (s *sharedIndexInformer) AddEventHandlerWithResyncPeriod(handler ResourceEventHandler, resyncPeriod time.Duration) {
    ...
    s.processor.addListener(listener)
    ...
}


// 分發
func (p *sharedProcessor) distribute(obj interface{}, sync bool) {
    p.listenersLock.RLock()
    defer p.listenersLock.RUnlock()

    if sync {
        for _, listener := range p.syncingListeners {
            listener.add(obj)
        }
    } else {
        for _, listener := range p.listeners {
            listener.add(obj)
        }
    }
}複製程式碼

command

定義: 命令模式(Command Pattern):將一個請求封裝為一個物件,從而使我們可用不同的請求對客戶進行引數化;對請求排隊或者記錄請求日誌,以及支援可撤銷的操作。命令模式是一種物件行為型模式,其別名為動作(Action)模式或事務(Transaction)模式。
kubernetes 的command 基於 github.com/spf13/cobra, 把命令變成了物件,比如cmdRollOut還實現了undo。

interator

和資料結構相關的package裡面用得比較多.如引用的庫jsoniter, btree, govalidator.

// golang標準庫 "go/token" 實現了Iterate方法,但是其實這裡更像vistor模式...
func (s *FileSet) Iterate(f func(*File) bool)複製程式碼

strategy

定義一系列演算法,讓這些演算法在執行時可以互換,使得分離演算法,符合開閉原則。物件有某個行為,但是在不同的場景中,該行為有不同的實現演算法。
實際上使用interface的都像是strategy模式,從這方面看strategy模式只是字面上的強調意義,實現上和interface 實現的factory模式只是強調的點不一樣,一個是強調建立型,一個是強調行為型
例子

// k8s每種物件都對應了一個strategy,決定了update,delete,get 的策略
// 比如 /k8s.io/kubernetes/pkg/registry/core/configmap/strategy.go

// strategy implements behavior for ConfigMap objects
type strategy struct {
    runtime.ObjectTyper
    names.NameGenerator
}

// Strategy is the default logic that applies when creating and updating ConfigMap
// objects via the REST API.
var Strategy = strategy{api.Scheme, names.SimpleNameGenerator}

// Strategy should implement rest.RESTCreateStrategy
var _ rest.RESTCreateStrategy = Strategy

// Strategy should implement rest.RESTUpdateStrategy
var _ rest.RESTUpdateStrategy = Strategy

func (strategy) NamespaceScoped() bool {
    return true
}

func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
    _ = obj.(*api.ConfigMap)
}

func (strategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
    cfg := obj.(*api.ConfigMap)

    return validation.ValidateConfigMap(cfg)
}

// Canonicalize normalizes the object after validation.
func (strategy) Canonicalize(obj runtime.Object) {
}

func (strategy) AllowCreateOnUpdate() bool {
    return false
}

func (strategy) PrepareForUpdate(ctx genericapirequest.Context, newObj, oldObj runtime.Object) {
    _ = oldObj.(*api.ConfigMap)
    _ = newObj.(*api.ConfigMap)
}

func (strategy) AllowUnconditionalUpdate() bool {
    return true
}

func (strategy) ValidateUpdate(ctx genericapirequest.Context, newObj, oldObj runtime.Object) field.ErrorList {
    oldCfg, newCfg := oldObj.(*api.ConfigMap), newObj.(*api.ConfigMap)

    return validation.ValidateConfigMapUpdate(newCfg, oldCfg)
}



// k8s.io/kubernetes/pkg/registry/core/configmap/storage/storage.go
// NewREST returns a RESTStorage object that will work with ConfigMap objects.
func NewREST(optsGetter generic.RESTOptionsGetter) *REST {
    store := &genericregistry.Store{
        NewFunc:                  func() runtime.Object { return &api.ConfigMap{} },
        NewListFunc:              func() runtime.Object { return &api.ConfigMapList{} },
        DefaultQualifiedResource: api.Resource("configmaps"),

        CreateStrategy: configmap.Strategy,
        UpdateStrategy: configmap.Strategy,
        DeleteStrategy: configmap.Strategy,
    }
    options := &generic.StoreOptions{RESTOptions: optsGetter}
    if err := store.CompleteWithOptions(options); err != nil {
        panic(err) // TODO: Propagate error up
    }
    return &REST{store}
}複製程式碼

state

狀態模式(State Pattern) :允許一個物件在其內部狀態改變時改變它的行為,物件看起來似乎修改了它的類。其別名為狀態物件(Objects for States),狀態模式是一種物件行為型模式。
分離狀態和行為,比方說一個狀態機的實現,就是一個標準的state 模式

// k8s.io/kubernetes/vendor/github.com/coreos/etcd/raft/node.go
// Node represents a node in a raft cluster.
type Node interface {
    ....
    Step(ctx context.Context, msg pb.Message) error

    ....

    // Status returns the current status of the raft state machine.
    Status() Status
    ....
}複製程式碼

memento

在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。這樣就可以將該物件恢復到原先儲存的狀態

// k8s.io/kubernetes/pkg/registry/core/service/portallocator/allocator.go
// NewFromSnapshot allocates a PortAllocator and initializes it from a snapshot.
func NewFromSnapshot(snap *api.RangeAllocation) (*PortAllocator, error) {
    pr, err := net.ParsePortRange(snap.Range)
    if err != nil {
        return nil, err
    }
    r := NewPortAllocator(*pr)
    if err := r.Restore(*pr, snap.Data); err != nil {
        return nil, err
    }
    return r, nil
}

func (r *PortAllocator) Snapshot(dst *api.RangeAllocation) error {
    snapshottable, ok := r.alloc.(allocator.Snapshottable)
    if !ok {
        return fmt.Errorf("not a snapshottable allocator")
    }
    rangeString, data := snapshottable.Snapshot()
    dst.Range = rangeString
    dst.Data = data
    return nil
}複製程式碼

廣義上看,deployment,statefulset等物件實現了rollback方法,也是類似memeto,比如deployment有各種version的replicaset的history備份,用於恢復歷史版本。詳細見k8s.io/kubernetes/pkg/controller/deployment/

flyweight / object pool

flyweight強調的是物件複用,和object pool的目的是一樣的。
One difference in that flyweights are commonly immutable instances, while resources acquired from the pool usually are mutable.
object pool在golang和kubernetes用得比較多,比如官方就有sync.pool

// k8s.io/kubernetes/staging/src/k8s.io/apiserver/pkg/storage/cacher.go

var timerPool sync.Pool

func (c *cacheWatcher) add(event *watchCacheEvent, budget *timeBudget) {
    ...

    t, ok := timerPool.Get().(*time.Timer)
    if ok {
        t.Reset(timeout)
    } else {
        t = time.NewTimer(timeout)
    }
    defer timerPool.Put(t)

    ....
}複製程式碼

iterpreter

直譯器模式定義一套語言文法,並設計該語言直譯器,使使用者能使用特定文法控制直譯器行為。
這個在kubernetes的各種程式碼/文件生成工具中用得很多。

chain_of_responsibility

也是一種組合模式.用的地方很多。職責鏈模式用於分離不同職責,並且動態組合相關職責。鏈物件包含當前職責物件以及下一個職責鏈。

// 這種wrapper 把函式處理交給childrens
// k8s.io/test-infra/velodrome/transform/plugins/multiplexer_wrapper.go
func NewMultiplexerPluginWrapper(plugins ...Plugin) *MultiplexerPluginWrapper {
    return &MultiplexerPluginWrapper{
        plugins: plugins,
    }
}

// 這種warpper 自己處理完了以後再交給clildrens
// k8s.io/test-infra/velodrome/transform/plugins/author_logger_wrapper.go
func NewAuthorLoggerPluginWrapper(plugin Plugin) *AuthorLoggerPluginWrapper {
    return &AuthorLoggerPluginWrapper{
        plugin: plugin,
    }
}

// 自己不處理,交給clildrens
...複製程式碼

visit

物件只要預留訪問者介面(如Accept)則後期為物件新增功能的時候就不需要改動物件。本質就是讓外部可以定義一種visit自己物件的方法,從這個角度看,只要visit作為一個函式傳遞就是一個visit模式。即

func(a *A)Visit(Vistor func(*A) error){
    Vistor(a)
}複製程式碼

例子

// k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/openapi.go
type SchemaVisitor interface {
    VisitArray(*Array)
    VisitMap(*Map)
    VisitPrimitive(*Primitive)
    VisitKind(*Kind)
    VisitReference(Reference)
}

// Schema is the base definition of an openapi type.
type Schema interface {
    // Giving a visitor here will let you visit the actual type.
    Accept(SchemaVisitor)

    // Pretty print the name of the type.
    GetName() string
    // Describes how to access this field.
    GetPath() *Path
    // Describes the field.
    GetDescription() string
    // Returns type extensions.
    GetExtensions() map[string]interface{}
}複製程式碼
// /k8s.io/kubernetes/pkg/kubectl/resource/builder.go
func (b *Builder) visitByName() *Result {
    ...

    visitors := []Visitor{}
    for _, name := range b.names {
        info := NewInfo(client, mapping, selectorNamespace, name, b.export)
        visitors = append(visitors, info)
    }
    result.visitor = VisitorList(visitors)
    result.sources = visitors
    return result
}複製程式碼

engineering patterns

code generator

相關文章