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
}
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
}
Top comments (1)
Thanks for creating this amazing blog post