Golang Design Patterns in Kubernetes

Golang Design Patterns in Kubernetes

I love reading open source code, you get to see different design patterns in action and learn about code architecture too. I have been looking into the Kubernetes codebase, and thought of penning down my research on Golang Design Patterns that have been used in different Kubernetes codebases, in hope that someone could also benefit from it.

Design patterns are typical solutions to common problems in software design. The most universal and high-level patterns are architectural patterns. All patterns can be categorized by their intent, or purpose. We covers two main groups of patterns:

  • Creational patterns provide object creation mechanisms that increase flexibility and reuse of existing code.
  • Behavioral patterns take care of effective communication and the assignment of responsibilities between objects.

Creational Patterns

Singleton

Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance.


package singleton

import (
    "sync"
)

type singleton struct {
}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

kubernetes/golang has very little use of global variables except for configuration management.

// https://github.com/kubernetes-sigs/controller-runtime/blob/master/alias.go

var (
	// GetConfigOrDie creates a *rest.Config for talking to a Kubernetes apiserver.
	// If --kubeconfig is set, will use the kubeconfig file at that location.  Otherwise will assume running
	// in cluster and use the cluster provided kubeconfig.
	//
	// Will log an error and exit if there is an error creating the rest.Config.
	GetConfigOrDie = config.GetConfigOrDie

	GetConfig = config.GetConfig

	// NewControllerManagedBy returns a new controller builder that will be started by the provided Manager.
	NewControllerManagedBy = builder.ControllerManagedBy

	// NewWebhookManagedBy returns a new webhook builder that will be started by the provided Manager.
	NewWebhookManagedBy = builder.WebhookManagedBy

	// NewManager returns a new Manager for creating Controllers.
	NewManager = manager.New


	// Log is the base logger used by controller-runtime.  It delegates
	// to another logr.Logger.  You *must* call SetLogger to
	// get any actual logging.
	Log = log.Log
)

// https://github.com/kubernetes-sigs/controller-runtime/blob/master/pkg/client/config/config.go

// GetConfigOrDie creates a *rest.Config for talking to a Kubernetes apiserver.
// If --kubeconfig is set, will use the kubeconfig file at that location.  Otherwise will assume running
// in cluster and use the cluster provided kubeconfig.
//
// Will log an error and exit if there is an error creating the rest.Config.
func GetConfigOrDie() *rest.Config {
	config, err := GetConfig()
	if err != nil {
		log.Error(err, "unable to get kubeconfig")
		os.Exit(1)
	}
	return config
}

Factory Method

Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

Golang:
package simplefactory

import "fmt"

//API is interface
type API interface {
	Say(name string) string
}

//NewAPI return Api instance by type
func NewAPI(t int) API {
	if t == 1 {
		return &hiAPI{}
	} else if t == 2 {
		return &helloAPI{}
	}
	return nil
}

//hiAPI is one of API implement
type hiAPI struct{}

//Say hi to name
func (*hiAPI) Say(name string) string {
	return fmt.Sprintf("Hi, %s", name)
}

//HelloAPI is another API implement
type helloAPI struct{}

//Say hello to name
func (*helloAPI) Say(name string) string {
	return fmt.Sprintf("Hello, %s", name)
}
Kubernetes:
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
}

https://github.com/kubernetes/apimachinery/blob/master/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)
}

func (f CodecFactory) CodecForVersions(encoder runtime.Encoder, decoder runtime.Decoder, encode runtime.GroupVersioner, decode runtime.GroupVersioner) runtime.Codec {
	// TODO: these are for backcompat, remove them in the future
	if encode == nil {
		encode = runtime.DisabledGroupVersioner
	}
	if decode == nil {
		decode = runtime.InternalGroupVersioner
	}
	return versioning.NewDefaultingCodecForScheme(f.scheme, encoder, decoder, encode, decode)
}

Abstract factory

Abstract Factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes. Abstract Factory defines an interface for creating all distinct products but leaves the actual product creation to concrete factory classes. Each factory type corresponds to a certain product variety.

Golang

Say, you need to buy a sports kit, a set of two different products: a pair of shoes and a shirt. You would want to buy a full sports kit of the same brand to match all the items.

If we try to turn this into the code, the abstract factory will help us create sets of products so that they would always match each other.

iSportsFactory.go: Abstract factory interface

package main

import "fmt"

type iSportsFactory interface {
    makeShoe() iShoe
    makeShirt() iShirt
}

func getSportsFactory(brand string) (iSportsFactory, error) {
    if brand == "adidas" {
        return &adidas{}, nil
    }

    if brand == "nike" {
        return &nike{}, nil
    }

    return nil, fmt.Errorf("Wrong brand type passed")
}

adidas.go: Concrete factory

package main

type adidas struct {
}

func (a *adidas) makeShoe() iShoe {
    return &adidasShoe{
        shoe: shoe{
            logo: "adidas",
            size: 14,
        },
    }
}

func (a *adidas) makeShirt() iShirt {
    return &adidasShirt{
        shirt: shirt{
            logo: "adidas",
            size: 14,
        },
    }
}

nike.go: Concrete factory

package main

type nike struct {
}

func (n *nike) makeShoe() iShoe {
    return &nikeShoe{
        shoe: shoe{
            logo: "nike",
            size: 14,
        },
    }
}

func (n *nike) makeShirt() iShirt {
    return &nikeShirt{
        shirt: shirt{
            logo: "nike",
            size: 14,
        },
    }
}

iShoe.go: Abstract product

package main

type iShoe interface {
    setLogo(logo string)
    setSize(size int)
    getLogo() string
    getSize() int
}

type shoe struct {
    logo string
    size int
}

func (s *shoe) setLogo(logo string) {
    s.logo = logo
}

func (s *shoe) getLogo() string {
    return s.logo
}

func (s *shoe) setSize(size int) {
    s.size = size
}

func (s *shoe) getSize() int {
    return s.size
}

adidasShoe.go: Concrete product

package main

type adidasShoe struct {
    shoe
}

nikeShoe.go: Concrete product

package main

type nikeShoe struct {
    shoe
}

iShirt.go: Abstract product

package main

type iShirt interface {
    setLogo(logo string)
    setSize(size int)
    getLogo() string
    getSize() int
}

type shirt struct {
    logo string
    size int
}

func (s *shirt) setLogo(logo string) {
    s.logo = logo
}

func (s *shirt) getLogo() string {
    return s.logo
}

func (s *shirt) setSize(size int) {
    s.size = size
}

func (s *shirt) getSize() int {
    return s.size
}

shirt.go: Concrete product

package main

type adidasShirt struct {
    shirt
}

type nikeShirt struct {
    shirt
}

main.go: Client code

package main

import "fmt"

func main() {
    adidasFactory, _ := getSportsFactory("adidas")
    nikeFactory, _ := getSportsFactory("nike")

    nikeShoe := nikeFactory.makeShoe()
    nikeShirt := nikeFactory.makeShirt()

    adidasShoe := adidasFactory.makeShoe()
    adidasShirt := adidasFactory.makeShirt()

    printShoeDetails(nikeShoe)
    printShirtDetails(nikeShirt)

    printShoeDetails(adidasShoe)
    printShirtDetails(adidasShirt)
}

func printShoeDetails(s iShoe) {
    fmt.Printf("Logo: %s", s.getLogo())
    fmt.Println()
    fmt.Printf("Size: %d", s.getSize())
    fmt.Println()
}

func printShirtDetails(s iShirt) {
    fmt.Printf("Logo: %s", s.getLogo())
    fmt.Println()
    fmt.Printf("Size: %d", s.getSize())
    fmt.Println()
}

Kubernetes

https://github.com/kubernetes/client-go/blob/v0.22.3/informers/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 is specific stuct
func (f * sharedInformerFactory) Apps () apps.Interface {
	return apps.New (f)
}

Builder

Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code. The Builder pattern is used when the desired product is complex and requires multiple steps to complete. In this case, several construction methods would be simpler than a single monstrous constructor. The potential problem with the multistage building process is that a partially built and unstable product may be exposed to the client. The Builder pattern keeps the product private until it’s fully built.

Golang
package builder

type Builder interface {
	Part1()
	Part2()
	Part3()
}

type Director struct {
	builder Builder
}

// NewDirector ...
func NewDirector(builder Builder) *Director {
	return &Director{
		builder: builder,
	}
}

//Construct Product
func (d *Director) Construct() {
	d.builder.Part1()
	d.builder.Part2()
	d.builder.Part3()
}

type Builder1 struct {
	result string
}

func (b *Builder1) Part1() {
	b.result += "1"
}

func (b *Builder1) Part2() {
	b.result += "2"
}

func (b *Builder1) Part3() {
	b.result += "3"
}

func (b *Builder1) GetResult() string {
	return b.result
}

type Builder2 struct {
	result int
}

func (b *Builder2) Part1() {
	b.result += 1
}

func (b *Builder2) Part2() {
	b.result += 2
}

func (b *Builder2) Part3() {
	b.result += 3
}

func (b *Builder2) GetResult() int {
	return b.result
}
Kubernetes

https://github.com/kubernetes/client-go/blob/v0.22.3/kubernetes/clientset.go#L586

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
}

https://github.com/kubernetes-sigs/controller-runtime/tree/master/pkg/builder

// Builder builds a Controller.
type Builder struct {
	forInput         ForInput
	ownsInput        []OwnsInput
	watchesInput     []WatchesInput
	mgr              manager.Manager
	globalPredicates []predicate.Predicate
	ctrl             controller.Controller
	ctrlOptions      controller.Options
	name             string
}

func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder {
	if blder.forInput.object != nil {
		blder.forInput.err = fmt.Errorf("For(...) should only be called once, could not assign multiple objects for reconciliation")
		return blder
	}
	input := ForInput{object: object}
	for _, opt := range opts {
		opt.ApplyToFor(&input)
	}

	blder.forInput = input
	return blder
}

// Watches exposes the lower-level ControllerManagedBy Watches functions through the builder.  Consider using
// Owns or For instead of Watches directly.
// Specified predicates are registered only for given source.
func (blder *Builder) Watches(src source.Source, eventhandler handler.EventHandler, opts ...WatchesOption) *Builder {
	input := WatchesInput{src: src, eventhandler: eventhandler}
	for _, opt := range opts {
		opt.ApplyToWatches(&input)
	}

	blder.watchesInput = append(blder.watchesInput, input)
	return blder
}

// WithEventFilter sets the event filters, to filter which create/update/delete/generic events eventually
// trigger reconciliations.  For example, filtering on whether the resource version has changed.
// Given predicate is added for all watched objects.
// Defaults to the empty list.
func (blder *Builder) WithEventFilter(p predicate.Predicate) *Builder {
	blder.globalPredicates = append(blder.globalPredicates, p)
	return blder
}

// WithOptions overrides the controller options use in doController. Defaults to empty.
func (blder *Builder) WithOptions(options controller.Options) *Builder {
	blder.ctrlOptions = options
	return blder
}

// WithLogger overrides the controller options's logger used.
func (blder *Builder) WithLogger(log logr.Logger) *Builder {
	blder.ctrlOptions.Log = log
	return blder
}

// Build builds the Application Controller and returns the Controller it created.
func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {
	if r == nil {
		return nil, fmt.Errorf("must provide a non-nil Reconciler")
	}
	if blder.mgr == nil {
		return nil, fmt.Errorf("must provide a non-nil Manager")
	}
	if blder.forInput.err != nil {
		return nil, blder.forInput.err
	}
	// Checking the reconcile type exist or not
	if blder.forInput.object == nil {
		return nil, fmt.Errorf("must provide an object for reconciliation")
	}

	// Set the ControllerManagedBy
	if err := blder.doController(r); err != nil {
		return nil, err
	}

	// Set the Watch
	if err := blder.doWatch(); err != nil {
		return nil, err
	}

	return blder.ctrl, nil
}

Prototype

Prototype is a creational design pattern that allows cloning objects, even complex ones, without coupling to their specific classes.

Conceptual Example

Let’s try to figure out the Prototype pattern using an example based on the operating system’s file system. The OS file system is recursive: the folders contain files and folders, which may also include files and folders, and so on.

Each file and folder can be represented by an inode interface. inode interface also has the clone function.

Both file and folder structs implement the print and clone functions since they are of the inode type. Also, notice the clone function in both file and folder. The clone function in both of them returns a copy of the respective file or folder. During the cloning, we append the keyword “_clone” for the name field.

inode.go: Prototype interface

package main

type inode interface {
    print(string)
    clone() inode
}

file.go: Concrete prototype

package main

import "fmt"

type file struct {
    name string
}

func (f *file) print(indentation string) {
    fmt.Println(indentation + f.name)
}

func (f *file) clone() inode {
    return &file{name: f.name + "_clone"}
}

folder.go: Concrete prototype

package main

import "fmt"

type folder struct {
    children []inode
    name      string
}

func (f *folder) print(indentation string) {
    fmt.Println(indentation + f.name)
    for _, i := range f.children {
        i.print(indentation + indentation)
    }
}

func (f *folder) clone() inode {
    cloneFolder := &folder{name: f.name + "_clone"}
    var tempChildren []inode
    for _, i := range f.children {
        copy := i.clone()
        tempChildren = append(tempChildren, copy)
    }
    cloneFolder.children = tempChildren
    return cloneFolder
}

main.go: Client code

package main

import "fmt"

func main() {
    file1 := &file{name: "File1"}
    file2 := &file{name: "File2"}
    file3 := &file{name: "File3"}

    folder1 := &folder{
        children: []inode{file1},
        name:      "Folder1",
    }

    folder2 := &folder{
        children: []inode{folder1, file2, file3},
        name:      "Folder2",
    }
    fmt.Println("\nPrinting hierarchy for Folder2")
    folder2.print("  ")

    cloneFolder := folder2.clone()
    fmt.Println("\nPrinting hierarchy for clone Folder")
    cloneFolder.print("  ")
}

output.txt: Execution result

Printing hierarchy for Folder2
  Folder2
    Folder1
        File1
    File2
    File3

Printing hierarchy for clone Folder
  Folder2_clone
    Folder1_clone
        File1_clone
    File2_clone
    File3_clone

https://github.com/kubernetes/apiserver/blob/master/pkg/apis/apiserver/v1/zz_generated.deepcopy.go

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdmissionConfiguration) DeepCopyInto(out *AdmissionConfiguration) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	if in.Plugins != nil {
		in, out := &in.Plugins, &out.Plugins
		*out = make([]AdmissionPluginConfiguration, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
	return
}

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

Behavioral Patterns

Observer

Observer is a behavioral design pattern that allows some objects to notify other objects about changes in their state. The Observer pattern provides a way to subscribe and unsubscribe to and from these events for any object that implements a subscriber interface.

Golang

In the e-commerce website, items go out of stock from time to time. There can be customers who are interested in a particular item that went out of stock. The customer subscribes only to the particular item he is interested in and gets notified if the item is available. Also, multiple customers can subscribe to the same product.

The major components of the observer pattern are:

Subject, the instance which publishes an event when anything happens.
Observer, which subscribes to the subject events and gets notified when they happen.

package observer

type subject interface {
    register(Observer observer)
    deregister(Observer observer)
    notifyAll()
}

type observer interface {
    update(string)
    getID() string
}

type item struct {
    observerList []observer
    name         string
    inStock      bool
}

func newItem(name string) *item {
    return &item{
        name: name,
    }
}
func (i *item) updateAvailability() {
    fmt.Printf("Item %s is now in stock\n", i.name)
    i.inStock = true
    i.notifyAll()
}
func (i *item) register(o observer) {
    i.observerList = append(i.observerList, o)
}

func (i *item) deregister(o observer) {
    i.observerList = removeFromslice(i.observerList, o)
}

func (i *item) notifyAll() {
    for _, observer := range i.observerList {
        observer.update(i.name)
    }
}

func removeFromslice(observerList []observer, observerToRemove observer) []observer {
    observerListLength := len(observerList)
    for i, observer := range observerList {
        if observerToRemove.getID() == observer.getID() {
            observerList[observerListLength-1], observerList[i] = observerList[i], observerList[observerListLength-1]
            return observerList[:observerListLength-1]
        }
    }
    return observerList
}

type customer struct {
    id string
}

func (c *customer) update(itemName string) {
    fmt.Printf("Sending email to customer %s for item %s\n", c.id, itemName)
}

func (c *customer) getID() string {
    return c.id
}

func main() {

    shirtItem := newItem("Nike Shirt")

    observerFirst := &customer{id: "abc@gmail.com"}
    observerSecond := &customer{id: "xyz@gmail.com"}

    shirtItem.register(observerFirst)
    shirtItem.register(observerSecond)

    shirtItem.updateAvailability()
}

// output

Item Nike Shirt is now in stock
Sending email to customer abc@gmail.com for item Nike Shirt
Sending email to customer xyz@gmail.com for item Nike Shirt
Kubernetes
func (s * sharedIndexInformer) AddEventHandlerWithResyncPeriod (handler ResourceEventHandler, resyncPeriod time.Duration) {
    ...
	s.processor.addListener (listener)
    ...
}


//Distribution
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 is behavioral design pattern that converts requests or simple operations into objects. The conversion allows deferred or remote execution of commands, storing command history, etc.

Golang
package command

import "fmt"

type Command interface {
	Execute()
}

type StartCommand struct {
	mb *MotherBoard
}

func NewStartCommand(mb *MotherBoard) *StartCommand {
	return &StartCommand{
		mb: mb,
	}
}

func (c *StartCommand) Execute() {
	c.mb.Start()
}

type RebootCommand struct {
	mb *MotherBoard
}

func NewRebootCommand(mb *MotherBoard) *RebootCommand {
	return &RebootCommand{
		mb: mb,
	}
}

func (c *RebootCommand) Execute() {
	c.mb.Reboot()
}

type MotherBoard struct{}

func (*MotherBoard) Start() {
	fmt.Print("system starting\n")
}

func (*MotherBoard) Reboot() {
	fmt.Print("system rebooting\n")
}

type Box struct {
	button1 Command
	button2 Command
}

func NewBox(button1, button2 Command) *Box {
	return &Box{
		button1: button1,
		button2: button2,
	}
}

func (b *Box) PressButton1() {
	b.button1.Execute()
}

func (b *Box) PressButton2() {
	b.button2.Execute()
}
Kubernetes

https://github.com/kubernetes/kubernetes/blob/master/cmd/kubectl/kubectl.go

kubernetes use of command is based on the cobra/cli package (github.com/spf13/cobra).

func main() {
	command := cmd.NewDefaultKubectlCommand()
	code := cli.Run(command)
	os.Exit(code)
}

Iterator

Iterator is a behavioral design pattern that allows sequential traversal through a complex data structure without exposing its internal details.

type collection interface {
    createIterator() iterator
}

type userCollection struct {
    users []*user
}

func (u *userCollection) createIterator() iterator {
    return &userIterator{
        users: u.users,
    }
}

type iterator interface {
    hasNext() bool
    getNext() *user
}

type userIterator struct {
    index int
    users []*user
}

func (u *userIterator) hasNext() bool {
    if u.index < len(u.users) {
        return true
    }
    return false

}
func (u *userIterator) getNext() *user {
    if u.hasNext() {
        user := u.users[u.index]
        u.index++
        return user
    }
    return nil
}

type user struct {
    name string
    age  int
}

func main() {

    user1 := &user{
        name: "a",
        age:  30,
    }
    user2 := &user{
        name: "b",
        age:  20,
    }

    userCollection := &userCollection{
        users: []*user{user1, user2},
    }

    iterator := userCollection.createIterator()

    for iterator.hasNext() {
        user := iterator.getNext()
        fmt.Printf("User is %+v\n", user)
    }
}
Kubernetes

https://github.com/kubernetes/apimachinery/blob/v0.21.0/pkg/runtime/serializer/json/json.go

// CaseSensitiveJSONIterator returns a jsoniterator API that's configured to be
// case-sensitive when unmarshalling, and otherwise compatible with
// the encoding/json standard library.
func CaseSensitiveJSONIterator() jsoniter.API {
	config := jsoniter.Config{
		EscapeHTML:             true,
		SortMapKeys:            true,
		ValidateJsonRawMessage: true,
		CaseSensitive:          true,
	}.Froze()
	// Force jsoniter to decode number to interface{} via int64/float64, if possible.
	config.RegisterExtension(&customNumberExtension{})
	return config
}

// StrictCaseSensitiveJSONIterator returns a jsoniterator API that's configured to be
// case-sensitive, but also disallows unknown fields when unmarshalling. It is compatible with
// the encoding/json standard library.
func StrictCaseSensitiveJSONIterator() jsoniter.API {
	config := jsoniter.Config{
		EscapeHTML:             true,
		SortMapKeys:            true,
		ValidateJsonRawMessage: true,
		CaseSensitive:          true,
		DisallowUnknownFields:  true,
	}.Froze()
	// Force jsoniter to decode number to interface{} via int64/float64, if possible.
	config.RegisterExtension(&customNumberExtension{})
	return config
}

Strategy

Strategy is a behavioral design pattern that turns a set of behaviors into objects and makes them interchangeable inside original context object. The original object, called context, holds a reference to a strategy object and delegates it executing the behavior. In order to change the way the context performs its work, other objects may replace the currently linked strategy object with another one.

Golang
type Payment struct {
	context  *PaymentContext
	strategy PaymentStrategy
}

type PaymentContext struct {
	Name, CardID string
	Money        int
}

func NewPayment(name, cardid string, money int, strategy PaymentStrategy) *Payment {
	return &Payment{
		context: &PaymentContext{
			Name:   name,
			CardID: cardid,
			Money:  money,
		},
		strategy: strategy,
	}
}

func (p *Payment) Pay() {
	p.strategy.Pay(p.context)
}

type PaymentStrategy interface {
	Pay(*PaymentContext)
}

type Cash struct{}

func (*Cash) Pay(ctx *PaymentContext) {
	fmt.Printf("Pay $%d to %s by cash", ctx.Money, ctx.Name)
}

type Bank struct{}

func (*Bank) Pay(ctx *PaymentContext) {
	fmt.Printf("Pay $%d to %s by bank account %s", ctx.Money, ctx.Name, ctx.CardID)

}
Kubernetes

https://github.com/kubernetes/kubernetes/blob/master/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}
}

Chain Of Responsibility

Chain of Responsibility is behavioral design pattern that allows passing request along the chain of potential handlers until one of them handles request. The pattern allows multiple objects to handle the request without coupling sender class to the concrete classes of the receivers. The chain can be composed dynamically at runtime with any handler that follows a standard handler interface.

Golang

Let’s look at the Chain of Responsibility pattern with the case of a hospital app. A hospital could have multiple departments such as:

  • Reception
  • Doctor
  • Medicine room
  • Cashier

Whenever any patient arrives, they first get to Reception, then to Doctor, then to Medicine Room, and then to Cashier (and so on). The patient is being sent through a chain of departments, where each department sends the patient further down the chain once their function is completed.

// department
type department interface {
    execute(*patient)
    setNext(department)
}

// reception
type reception struct {
    next department
}

func (r *reception) execute(p *patient) {
    if p.registrationDone {
        fmt.Println("Patient registration already done")
        r.next.execute(p)
        return
    }
    fmt.Println("Reception registering patient")
    p.registrationDone = true
    r.next.execute(p)
}

func (r *reception) setNext(next department) {
    r.next = next
}

// doctor
type doctor struct {
    next department
}

func (d *doctor) execute(p *patient) {
    if p.doctorCheckUpDone {
        fmt.Println("Doctor checkup already done")
        d.next.execute(p)
        return
    }
    fmt.Println("Doctor checking patient")
    p.doctorCheckUpDone = true
    d.next.execute(p)
}

func (d *doctor) setNext(next department) {
    d.next = next
}

// medical
type medical struct {
    next department
}

func (m *medical) execute(p *patient) {
    if p.medicineDone {
        fmt.Println("Medicine already given to patient")
        m.next.execute(p)
        return
    }
    fmt.Println("Medical giving medicine to patient")
    p.medicineDone = true
    m.next.execute(p)
}

func (m *medical) setNext(next department) {
    m.next = next
}

// cashier

type cashier struct {
    next department
}

func (c *cashier) execute(p *patient) {
    if p.paymentDone {
        fmt.Println("Payment Done")
    }
    fmt.Println("Cashier getting money from patient patient")
}

func (c *cashier) setNext(next department) {
    c.next = next
}

// patient
type patient struct {
    name              string
    registrationDone  bool
    doctorCheckUpDone bool
    medicineDone      bool
    paymentDone       bool
}



func main() {

    cashier := &cashier{}

    //Set next for medical department
    medical := &medical{}
    medical.setNext(cashier)

    //Set next for doctor department
    doctor := &doctor{}
    doctor.setNext(medical)

    //Set next for reception department
    reception := &reception{}
    reception.setNext(doctor)

    patient := &patient{name: "abc"}
    //Patient visiting
    reception.execute(patient)
}
// Output
Reception registering patient
Doctor checking patient
Medical giving medicine to patient
Cashier getting money from patient patient

Visitor

Visitor is a behavioral design pattern that allows adding new behaviors to existing class hierarchy without altering any existing code. The Visitor pattern lets you add behavior to a struct without actually modifying the struct. Let’s say you are the maintainer of a lib which has different shape structs such as:

  • Square
  • Circle
  • Triangle

Each of the above shape structs implements the common shape interface.

Once people in your company started to use your awesome lib, you got flooded with feature requests. Let’s review one of the simplest ones: a team requested you to add the getArea behavior to the shape structs.

There are many options to solve this problem.

The first option that comes to the mind is to add the getArea method directly into the shape interface and then implement it in each shape struct. This seems like a go-to solution, but it comes at a cost. As the maintainer of the library, you don’t want to risk breaking your precious code each time someone asks for another behavior. Still, you do want other teams to extend your library somehow.

The second option is that the team requesting the feature can implement the behavior themselves. However, this is not always possible, as this behavior may depend on the private code.

The third option is to solve the above problem using the Visitor pattern. We start by defining a visitor interface like this:

type visitor interface {
   visitForSquare(square)
   visitForCircle(circle)
   visitForTriangle(triangle)
}

The functions visitForSquare(square), visitForCircle(circle), visitForTriangle(triangle) will let us add functionality to squares, circles and triangles respectively.

type shape interface {
    getType() string
    accept(visitor)
}

type square struct {
    side int
}

func (s *square) accept(v visitor) {
    v.visitForSquare(s)
}

func (s *square) getType() string {
    return "Square"
}

type circle struct {
    radius int
}

func (c *circle) accept(v visitor) {
    v.visitForCircle(c)
}

func (c *circle) getType() string {
    return "Circle"
}

type rectangle struct {
    l int
    b int
}

func (t *rectangle) accept(v visitor) {
    v.visitForrectangle(t)
}

func (t *rectangle) getType() string {
    return "rectangle"
}

type visitor interface {
    visitForSquare(*square)
    visitForCircle(*circle)
    visitForrectangle(*rectangle)
}

type areaCalculator struct {
    area int
}

func (a *areaCalculator) visitForSquare(s *square) {
    // Calculate area for square.
    // Then assign in to the area instance variable.
    fmt.Println("Calculating area for square")
}

func (a *areaCalculator) visitForCircle(s *circle) {
    fmt.Println("Calculating area for circle")
}
func (a *areaCalculator) visitForrectangle(s *rectangle) {
    fmt.Println("Calculating area for rectangle")
}

type middleCoordinates struct {
    x int
    y int
}

func (a *middleCoordinates) visitForSquare(s *square) {
    // Calculate middle point coordinates for square.
    // Then assign in to the x and y instance variable.
    fmt.Println("Calculating middle point coordinates for square")
}

func (a *middleCoordinates) visitForCircle(c *circle) {
    fmt.Println("Calculating middle point coordinates for circle")
}
func (a *middleCoordinates) visitForrectangle(t *rectangle) {
    fmt.Println("Calculating middle point coordinates for rectangle")
}

func main() {
    square := &square{side: 2}
    circle := &circle{radius: 3}
    rectangle := &rectangle{l: 2, b: 3}

    areaCalculator := &areaCalculator{}

    square.accept(areaCalculator)
    circle.accept(areaCalculator)
    rectangle.accept(areaCalculator)

    fmt.Println()
    middleCoordinates := &middleCoordinates{}
    square.accept(middleCoordinates)
    circle.accept(middleCoordinates)
    rectangle.accept(middleCoordinates)
}
Kubernetes

https://github.com/kubernetes/kube-openapi/blob/master/pkg/util/proto/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
}

Resources

Subscribe to Farhan Aly

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe