GO: Design Pattern

程亦
2023-12-01

Background

In general, a pattern has four essential elements:
pattern name, problem, solution, consequence

Creational Pattern

Singleton

singleton is used when we need the object to be unique globally. the key point is, we set the object to be private for this package, then create a Get() function to use this object.

// Singleton definition.
type Singleton struct {
    Name string
}

var (
    instance *Singleton
)

// NewSingleton return singleton instance.
func NewSingleton() *Singleton {
    if instance == nil {
        instance = &Singleton{Name: "singleton"}
    }
    return instance
}

in the example above, when we r trying to get Singleton struct, we only get one struct globally.

Abstract Factory

Different struct realize the same interface. Also know as kit.

e.g. we r going to create a iSportsFactory interface, and both the Nike struct and the Adidas struct have the makeShoe() and makeShirt() method.

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

and then we can create adidas struct and nike struct, both struct already implemented two makeShoe() and makeShirt() functions.

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,
        },
    }
}
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,
        },
    }
}

and for both the adidas struct and the nike struct can be used as the iSportsFactory interface.

Factory Method

in abstract factory pattern, functions in interface have fixed input and out params, sometimes we can abstract the type of input and output as interface.

So Factory Method means “use factory as a input or output method”

In factory method, we’ll encapsulate functions into interface.

package method

import "fmt"

type FoodKind int

const (
    MeatKind FoodKind = iota
    FruitKind
    VegetableKind
    NutKind
)

//Food 食物接口
type Food interface {
    Eat()
}

//肉-食物接口的实现
type meat struct {
}

func (t *meat) Eat() {
    fmt.Println("Eat meat")
}

//水果-食物接口的实现
type fruit struct {
}

func (t *fruit) Eat() {
    fmt.Println("Eat fruit")
}

//蔬菜-食物接口的实现
type vegetable struct {
}

func (t *vegetable) Eat() {
    fmt.Println("Eat vegetable")
}

//-------------干饭人的分割线-----------
//食物工厂
type Factory interface {
    NewFood(k FoodKind) Food //这个函数是不是和简单工厂模式一模一样,而且分割线上面的代码也是一模一样
}

//肉厂
type MeatFactory struct {
}

func (t MeatFactory) NewFood(k FoodKind) Food {
    return &meat{}
}

//水果厂
type FruitFactory struct {
}

func (t FruitFactory) NewFood(k FoodKind) Food {
    return &fruit{}
}

//蔬菜厂
type VegetableFactory struct {
}

func (t VegetableFactory) NewFood(k FoodKind) Food {
    return &vegetable{}
}

Usage

func main() {
    fmt.Println("factory method pattern")
    method.MeatFactory{}.NewFood(method.MeatKind).Eat() //去肉厂吃肉
    method.FruitFactory{}.NewFood(method.FruitKind).Eat() //去水果厂吃水果
    method.VegetableFactory{}.NewFood(method.VegetableKind).Eat()
    method.NutFactory{}.NewFood(method.NutKind).Eat()    
}

Prototype

we use prototype when we r trying to create duplicated objects.

package prototype

// Cloneable接口,动物必须实现这个接口
type Cloneable interface {
	Clone() Cloneable
}

// 克隆实验室
type CloneLab struct {
	animals map[string]Cloneable
}

func NewPrototypeManager() *CloneLab {
	return &CloneLab{animals:make(map[string]Cloneable)}
}

// 获取克隆
func (p *CloneLab) Get(name string) Cloneable {
	return p.animals[name]
}

// set动物当前属性
func (p *CloneLab) Set(name string,prototype Cloneable) {
	p.animals[name] = prototype
}

prototype_test.go:

package prototype

import (
	"testing"
)

var lab *Cloneable

// 羊
type Sheep struct {
	name string
	weight int
}

func (s *Sheep) Clone() Cloneable {
	tc := *s
	return &tc
}

// 牛
type Cow struct {
	name string
	gender bool
}

func (c *Cow) Clone() Cloneable  {
	newCow := &Cow{
		name:  c.name,
		gender: c.gender,
	}
	return newCow
}


func TestClone(t *testing.T) {
	sheep1 := &Sheep{
		name:   "sheep",
		weight: 10,
	}

	sheep2 := sheep1.Clone()
	
  // 这里地址肯定不同,因为是一个新的实例
	if sheep1 == sheep2 {
		t.Fail()
	}
}

func TestCloneFromManager(t *testing.T) {
	lab := NewCloneLab()

	lab.Set("cow", &Cow{name: "i am cow", gender: true})

	c := lab.Get("cow").Clone()

	cw := c.(*Cow)
	if cw.name != "i am cow" {
		t.Fatal("error")
	}
}

in the code above, we created an interface, and the function in this interface will return this interface itself. And when we create a struct to inherit this interface, we can clone new object via the clone function.

Builder

Builder is used when the desired object requires complex steps to complete. Builder allows us to separate the constructions of a complex object.

The builder start with a struct which has an interface attribution. The methods in this interface will return an initiated object.

With builder, we can initiate different objects with the same construction code.

package builder
 
type PizzaProcess interface {
    PizzaDough() PizzaProcess
    PizzaSauce() PizzaProcess
    PizzaTopping() PizzaProcess
    GetPizza() PizzaProduct
}
 
type PizzaProgress struct {
    builder PizzaProcess
}

func (f *PizzaProgress) Construct() {
    f.builder.PizzaSauce().PizzaTopping().PizzaDough()
}
 
func (f *PizzaProgress) SetPizza(b PizzaProcess) {
    f.builder = b
}
 
type PizzaProduct struct {
    Dough   string
    Sauce   string
    Topping string
}
 
type VegPizza struct {
    v PizzaProduct
}
 
func (veg *VegPizza) PizzaDough() PizzaProcess {
    veg.v.Dough = "Small"
    return veg
}
 
func (veg *VegPizza) PizzaSauce() PizzaProcess {
    veg.v.Sauce = "Bechamel"
    return veg
}
 
func (veg *VegPizza) PizzaTopping() PizzaProcess {
    veg.v.Topping = "Mushrooms"
    return veg
}
 
func (veg *VegPizza) GetPizza() PizzaProduct {
    return veg.v
}
 
type NonVegPizza struct {
    n PizzaProduct
}
 
func (non *NonVegPizza) PizzaDough() PizzaProcess {
    non.n.Dough = "Large"
    return non
}
 
func (non *NonVegPizza) PizzaSauce() PizzaProcess {
    non.n.Sauce = "Pesto"
    return non
}
 
func (non *NonVegPizza) PizzaTopping() PizzaProcess {
    non.n.Topping = "Pepperoni"
    return non
}
 
func (non *NonVegPizza) GetPizza() PizzaProduct {
    return non.n
}

Structural Patterns

Adapter

Adapter pattern works as a bridge between two incompatible interfaces.
we have a charger(client) with lighting port, and can charge MAC, but since windows need to use USB, so we cannot charge windows with this charger(client) , but if we use adapter, then we can use this charger to charge windows device.

package main

import "fmt"

type client struct {
}

func (c *client) excuteProgram(s system) {
	s.chargeWithLighting()
}

type system interface {
	chargeWithLighting()
}

type mac struct {
}

func (m *mac) chargeWithLighting() {
	fmt.Println("MAC: I'm charging")
}

type windows struct{}

func (w *windows) chargeWithUSB() {
	fmt.Println("windows: I'm charging")
}

type windowsAdapter struct {
	windowMachine *windows
}

func (w *windowsAdapter) chargeWithLighting() {
	fmt.Println("Adapter is working")
	w.windowMachine.chargeWithUSB()
}

func main() {
	client := &client{}
	mac := &mac{}
	client.excuteProgram(mac)

	windowsMachine := &windows{}
	windowsMachineAdapter := &windowsAdapter{
		windowMachine: windowsMachine,
	}
	client.excuteProgram(windowsMachineAdapter)
}

Bridge

to separate the abstraction and implementation. for computer systems we have mac and windows, for printer brand, we have epson and hp. if we want to execute the behaviour of “print”, then actually we don’t need to create 2*2 methods. We can use bridge mode to separate the computer and printer into interface.

package main

import "fmt"

type computer interface {
	print()
	setPrinter(printer)
}

type printer interface {
	printFile()
}

type mac struct {
	printer printer
}

func (m *mac) setPrinter(p printer) {
	m.printer = p
}

func (m *mac) print() {
	m.printer.printFile()
}

type windows struct {
	printer printer
}

func (w *windows) setPrinter(p printer) {
	w.printer = p
}

func (w *windows) print() {
	w.printer.printFile()
}

type epson struct {
}

func (p *epson) printFile() {
	fmt.Println("EPSON: I'm printing")
}

type hp struct {
}

func (p *hp) printFile() {
	fmt.Println("HP: I'm printing")
}

func main() {
	hpPrinter := &hp{}
	epsonPrinter := &epson{}

	macComputer := &mac{}

	macComputer.setPrinter(hpPrinter)
	macComputer.print()
	fmt.Println()

	macComputer.setPrinter(epsonPrinter)
	macComputer.print()
	fmt.Println()

	winComputer := &windows{}

	winComputer.setPrinter(hpPrinter)
	winComputer.print()
	fmt.Println()

	winComputer.setPrinter(epsonPrinter)
	winComputer.print()
	fmt.Println()
}

composite

Composite allows composing objects into a tree-like structure and work with the it as if it was a singular object.

like suppose we want to search a keyword in a folder, this

package main

import "fmt"

type component interface {
	search(string)
}

type file struct {
	name string
}

func (f *file) search(keyword string) {
	fmt.Printf("file %s: searching for %s\n", f.name, keyword)
}

func (f *file) getName() string {
	return f.name
}

type folder struct {
	components []component
	name string
}

func (f *folder) search(keyword string) {
	fmt.Printf("folder %s: searching for %s\n", f.name, keyword)
	for _, component := range f.components {
		component.search(keyword)
	}
}

func (f *folder) add(c component) {
	f.components = append(f.components, c)
}

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

	folder1 := &folder{
		name: "Folder1",
	}

	folder1.add(file1)

	folder2 := &folder{
		name: "Folder2",
	}
	folder2.add(file2)
	folder2.add(file3)
	folder2.add(folder1)

	folder2.search("rose")
}

decorator

there is a basic struct with its functions, use an interface to represent this struct, then use this interface as an attribution inside an advanced struct. So this advanced struct can implement these functions based on the basic struct.
for example, we have a pizza interface, this interface has a getPrice() function. then we can set a topping decorator struct, which contains this pizza interface as an attribute, then we can add price based on this interface function.

package main

import "fmt"

type pizza interface {
	getPrice() int
}

type basicPizza struct {
}

func (p *basicPizza) getPrice() int {
	return 15
}1

type toppingMeat struct {
	pizza pizza
}

func (m *toppingMeat) getPrice() int {
	return m.pizza.getPrice() + 7
}

func main() {
	myPizza := &basicPizza{}
	meatPizza := &toppingMeat{
		pizza: myPizza,
	}
	fmt.Println(meatPizza.getPrice())
}

Facade

Facade shields complicated systems by providing a simplified interface of classes. so the client can ignore the complicated internal implementation.
Basically it will hide non-Facade functions as lower case functions. And will provide Facade functions as UpperCase functions.

Flyweight

Flyweight is a software design pattern. A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects; it is a way to use objects in large numbers when a simple repeated representation would use an unacceptable amount of memory.

like if we want to store any objects in our memory, we can consider first whether we can store the pointers which represents these objects, rather than store these objects directly.

proxy

The proxy object has the same interface as a service, which makes it interchangeable with a real object when passed to a client. like nginx or some middleware.

The key point is the principal and the proxy will implement the same interface; and the proxy struct will include the principal struct as an attribution.

package main

type server interface {
    handleRequest(string, string) (int, string)
}

type nginx struct {
    application       *application
    maxAllowedRequest int
    rateLimiter       map[string]int
}

func newNginxServer() *nginx {
    return &nginx{
        application:       &application{},
        maxAllowedRequest: 2,
        rateLimiter:       make(map[string]int),
    }
}

func (n *nginx) handleRequest(url, method string) (int, string) {
    allowed := n.checkRateLimiting(url)
    if !allowed {
        return 403, "Not Allowed"
    }
    return n.application.handleRequest(url, method)
}

func (n *nginx) checkRateLimiting(url string) bool {
    if n.rateLimiter[url] == 0 {
        n.rateLimiter[url] = 1
    }
    if n.rateLimiter[url] > n.maxAllowedRequest {
        return false
    }
    n.rateLimiter[url] = n.rateLimiter[url] + 1
    return true
}

type application struct {
}

func (a *application) handleRequest(url, method string) (int, string) {
    if url == "/app/status" && method == "GET" {
        return 200, "Ok"
    }

    if url == "/create/user" && method == "POST" {
        return 201, "User Created"
    }
    return 404, "Not Ok"
}

Behavioral Patterns

Chain of Responsibility

allows passing request along the chain of potential handlers until one of them handles request.

package main

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

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
}

type doctor struct {
    next department
}


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

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
}

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
}

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
}

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

so for the example above, all department is an implementation of interface department, and by checking the patient status, all the department can determine whether to execute current process under current department.

Command

can use when need to do some duplicated work. a good illustration here:https://www.sohamkamani.com/golang/command-pattern/

func main() {
	// initialize a new resaurant
	r := NewResteraunt()

	// create the list of tasks to be executed
	tasks := []Command{
		r.MakePizza(2),
		r.MakeSalad(1),
		r.MakePizza(3),
		r.CleanDishes(),
		r.MakePizza(4),
		r.CleanDishes(),
	}

	// create the cooks that will execute the tasks
	cooks := []*Cook{
		&Cook{},
		&Cook{},
	}

	// Assign tasks to cooks alternating between the existing
	// cooks.
	for i, task := range tasks {
		// Using the modulus of the current task index, we can
		// alternate between different cooks
		cook := cooks[i%len(cooks)]
		cook.Commands = append(cook.Commands, task)
	}

	// Now that all the cooks have their commands, we can call
	// the `executeCommands` method that will have each cook
	// execute their respective commands
	for i, c := range cooks {
		fmt.Println("cook", i, ":")
		c.executeCommands()
	}
}

reference: https://blog.csdn.net/qibin0506/article/details/50812611

Interpreter

we will pass different expression(grammer) into a interpreter, and the interpreter will generate different result according to different expression.

package main

import "fmt"

// Expression    
type Expression interface {
	Interpreter(a, b int) int
}

// AddExpression Add
type AddExpression struct {
}

func (t AddExpression) Interpreter(a, b int) int {
	return a + b
}

// SubExpression Sub
type SubExpression struct {
}

func (t SubExpression) Interpreter(a, b int) int {
	return a - b
}

// MulExpression multiply
type MulExpression struct {
}

func (t MulExpression) Interpreter(a, b int) int {
	return a * b
}

// PowerExpression power
type PowerExpression struct {
}

func (t PowerExpression) Interpreter(a, b int) int {
	result := 1
	for i := 0; i < b; i++ {
		result *= a
	}
	return result
}

type CalcParser struct {
	expr1 Expression
	expr2 Expression
}
// according to different interpreter,to process 3 parameters and generate a result
func (t CalcParser) interperter(a, b, c int) int {
	return t.expr1.Interpreter(a, t.expr2.Interpreter(b, c))
}

func main() {
	add := AddExpression{}
	power := PowerExpression{}
	p := CalcParser{expr1: add, expr2: power}
	fmt.Println(p.interperter(1, 2, 3)) // interpreted as 1+ 2^3, the result is 9
	p.expr2 = MulExpression{}
	fmt.Println(p.interperter(1, 2, 3)) // interpreted as  1+ 2*3 the result is 7
}

Iterator

package main

import "fmt"

type Ints []int

func (i Ints) Iterator() *Iterator {
	return &Iterator{
		data:  i,
		index: 0,
	}
}

type Iterator struct {
	data  Ints
	index int
}

func (i *Iterator) HasNext() bool {
	return i.index < len(i.data)
}

func (i *Iterator) Next() (v int) {
	v = i.data[i.index]
	i.index++
	return v
}

func main() {
	ints := Ints{1, 2, 3}
	for it := ints.Iterator(); it.HasNext(); {
		fmt.Println(it.Next())
	}
}

Mediator

a system may contain a lot of modules, each modules might import other modules. Mediator is to reduce the coupling between these modules. for example, in a chatting room, when a client try to chat with other members, they can send the msg to the chat room first, then the chat room will redirect this msg to other clients.

//ChatRoom mediacotr
type ChatRoom struct{}

var chatRoom = NewChatRoom()

//NewChatRoom Initialize
func NewChatRoom() *ChatRoom {
    return &ChatRoom{}
}

//ShowMessage
func (cr *ChatRoom) ShowMessage(user *User, msg string) {
    fmt.Printf("%s: [ %s ]: %s \n",
        time.Now().Format("2006-01-02 15:04:05"),
        user.GetName(),
        msg)
}

//User
type User struct {
    Name string
}

//NewUser 实例化用户类
func NewUser(name string) *User {
    return &User{
        Name: name,
    }
}

//SendMessage 用户类使用中介者发送消息
func (u *User) SendMessage(msg string) {
    chatRoom.ShowMessage(u, msg)
}

//GetName 获取用于昵称
func (u *User) GetName() string {
    return u.Name
}

Memento

Memento will keep a statu of an object, and can help us roll back.

//Memento 备忘录类
type Memento struct {
    state string
}

//NewMemento 实例化备忘录类
func NewMemento(st string) *Memento {
    return &Memento{
        state: st,
    }
}

//GetState 获取备忘录类的状态
func (m *Memento) GetState() string {
    return m.state
}

Observer

Used in the Publisher-Subscriber mode

State

used in infinite state machine, use a switch-case logic to process the object status

Strategy

in cache scenario, u can choose different cache eviction policy, with strategy pattern, u can change the strategy pattern without restart the program.
https://golangbyexample.com/strategy-design-pattern-golang/

Template

Template lets you define a template or algorithm for a particular operation. a bit like abstract factory in implementation.

visitor

Visitor lets you add behaviour to a struct without actually modifying the struct.
Suppose there is 1 interface and several structs which implemented this interface:

type InternalInterface interface {
	InternalFun()
}

type InternalStruct1 struct {
}
func (s1 *InternalStruct1) InternalFun() {}

type InternalStruct2 struct {
}
func (s2 *InternalStruct2) InternalFun() {}

we don’t want to change the internal struct1 and the internal struct2(or maybe they were encapsulated), but we want to extend with some functions. then we can add an accept function to internal interface, and create a visitor interface to implement these functions.

type InternalInterface interface{
	InternalFun()
	accept(visitor)
}

type visitor interface {
	VisitInternalStruct1()
	VisitInternalStruct2()
}

after this, we can create struct which implement this visitor interface. So by implementing the accept function, outer struct can visit the interval struct.
a good example here:
https://medium.com/@felipedutratine/visitor-design-pattern-in-golang-3c142a12945a

 类似资料:

相关阅读

相关文章

相关问答