DEV Community

Cover image for Design Patterns in Kubernetes Codebase
Farhan
Farhan

Posted on • Updated on

Design Patterns in Kubernetes Codebase

Published originally on my blog post here

I love reading open source code, you get to learn 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
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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,
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

adidasShoe.go: Concrete product

package main

type adidasShoe struct {
    shoe
}

Enter fullscreen mode Exit fullscreen mode

nikeShoe.go: Concrete product

package main

type nikeShoe struct {
    shoe
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

shirt.go: Concrete product

package main

type adidasShirt struct {
    shirt
}

type nikeShirt struct {
    shirt
}
Enter fullscreen mode Exit fullscreen mode

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()
}

Enter fullscreen mode Exit fullscreen mode
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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"}
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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("  ")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
}

Enter fullscreen mode Exit fullscreen mode
// 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
Enter fullscreen mode Exit fullscreen mode
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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode
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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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)

}
Enter fullscreen mode Exit fullscreen mode
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}
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
// Output
Reception registering patient
Doctor checking patient
Medical giving medicine to patient
Cashier getting money from patient patient
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

Resources

Discussion (0)