年度收官!聊聊 Go 与 GOF 模式的那些事儿(持续🔥热更中)

932 阅读1小时+

概述

写在前面

我们尚未正式开始学习,让我们先理智地认识到,设计模式 (Design Patterns) 并非解决所有问题的银弹(Silver bullet)。确切来说,它们只是一套工具,帮助我们更好地总结经验和最佳实践,以实现优雅且易读的代码。

尽管设计模式具有其重要性,但这并不能补充深入学习和理解数据结构与算法的必要性。在提升代码效率和性能方面,掌握数据结构和算法才是核心关键。这是每一个称职的程序员都必须不断提升和深化的领域。

在实际应用中,设计模式和数据结构、算法是相辅相成的。我们需要结合运用这些工具和知识,才能解决问题并编写出高效、优秀的代码。

此外,持续重构是一个极其重要的过程。通过改善代码的结构和设计,我们方能使代码更加稳健并且易于维护。

因此,在学习设计模式的同时,我们也需要密切关注数据结构和算法的学习。通过不断的实战操作和深化理解,我们会成长为更出色的软件开发者,有能力编写高质量的代码。

image.png

我们所有的示例代码都采用了 "模式-调用" 分离的结构,以更直观地展示在 Go 语言中使用设计模式的魅力。几乎都没有提供 Output 输出,还是鼓励大家亲自动手敲一遍代码,毕竟计算机属于工科,再多的理论也终将会是过眼云烟。

学习网站

在线学习网站: Refactoring.Guru

image.png

GOF 模式

"GOF" 是指 《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software) 这本经典的设计模式书籍的四位作者首字母缩写,也被称为 "Gang of Four"。

或者 "GOF" 我们可以理解为 Friends Of Go 🤔️🤔️🤔️

image.png

GOF 模式(23 种设计模式)可以按照一些常见的分类方式进行组织,例如 "创建型模式"、"结构型模式" 和 "行为型模式"。这种分类方式可以帮助我们理解和组织这些模式的概念和应用场景,但并不意味着你必须按照特定的顺序学习或使用它们。具体如下:

类型 模式 流行度 简单描述
创建型模式 单例模式 🌟🌟 确保类只有一个实例,并提供一个全局访问点。
工厂方法模式 🌟🌟🌟 定义一个创建对象的接口,但由子类决定要实例化的类是哪一个。
抽象工厂模式 🌟🌟 提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
建造者模式 🌟🌟 使用简单对象和逐步构建的方式复杂对象进行构建的创建型模式。
原型模式 🌟 用于创建对象的种类,并通过拷贝这些原型创建新的对象。
结构型模式 适配器模式 🌟🌟🌟 允许对象使用不同的接口进行交互。
桥接模式 🌟 将抽象化与实现化解耦,使得二者可以独立变化。
组合模式 🌟🌟 将对象组成树形结构以表示"部分-整体"的层次结构。
装饰器模式 🌟🌟 动态地给一个对象添加一些额外的职责,就增加功能来说,比生成子类更为灵活。
外观模式 🌟🌟🌟 为子系统中的一组接口提供一个一致的界面。
享元模式 🌟 通过与其他相似对象共享数据来减小内存使用量,或者计算或网络负载。
代理模式 🌟🌟 为其他对象提供一个代理以控制对这个对象的访问。
行为型模式 责任链模式 🌟🌟 为请求创建了一个接收者对象的链,这些接收者中包含有请求的处理代码。
命令模式 🌟🌟🌟 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
解释器模式 🌟 定义语法的一种方式,用于解释一个表达式。
迭代器模式 🌟🌟🌟 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露其内部的表示。
中介者模式 🌟 用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地互相引用,从而使其耦合松散。
备忘录模式 🌟 在不破坏对象的封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便将来恢复。
观察者模式 🌟🌟🌟 对象之间存在一对多的依赖关系,当一个对象状态改变时,所有依赖于它的对象都会收到通知。
状态模式 🌟🌟 对象的行为依赖于它的状态(即,它的属性),并且可以根据它的状态改变而改变其行为。
策略模式 🌟🌟🌟 定义了一系列的算法,并将每一个算法封装起来,使它们可以互相替换。
模板方法模式 🌟🌟 在不改变模板结构的前提下,重新定义模板的某些特定步骤。
访问者模式 🌟 主要将数据结构与数据操作分离,解决数据结构和操作之间的耦合性。

我们用 🌟 的数量来代表模式在 Go 语言中的流行程度,🌟 颗星表示较少使用(不常用或由于 Go 语言的特性而不太适用此模式),🌟🌟 颗星表示中等使用,🌟🌟🌟 颗星表示广泛使用。

创建型模式

单例模式

单例模式(Singleton Pattern)是一种创建型设计模式,它确保类只有一个实例,并提供全局访问点以供其他对象使用。

image.png

代码示例

创建了一个Logger类型的单例,并用它来写日志。

// gof/singleton.go
package gof

import (
    "fmt"
    "sync"
    "time"
)

type Logger struct {
    mu sync.Mutex
}

var instance *Logger
var once sync.Once

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

func (l *Logger) Log(message string, index ...int) {
    l.mu.Lock()
    defer l.mu.Unlock()

    fmt.Printf("[%s] %s %d\n", time.Now().Format("2006-01-02 15:04:05"), message, index[0])
}

这个函数返回的始终是同一个Logger的实例。由sync.Once类型变量once保证的,无论多少协程去调用这个函数,实例都只创建一次。Logger结构体内有一个互斥锁mu,这把锁就是用来保护Log方法的并发访问的。

// main.go
package main

import (
    "sync"

    "codebase/gof"
)

func main() {
    wg := sync.WaitGroup{}

    for i := 0; i < 100000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            logger := gof.GetInstance()
            logger.Log("this is a message", i) // 让每一个协程都去写日志
        }(i)
    }

    wg.Wait()
}

综合评估

外界有些言论会说单例模式是一种过时的设计模式。为什么会有这样的观点呢?其实单例模式确实有一些潜在的问题,我们看下面的优劣分析:

类别 内容
优势
  1. 全局资源控制:当网络应用需要创建一些全局的对象,其中经常包括数据库连接池、线程池、系统中的配置对象、应用缓存和日志对象等,使用单例模式能够方便地控制其数量,确保系统中只有一个实例存在。
  2. 对象创建成本高:如果对象创建的成本较高,例如,它需要消耗大量的系统资源,或者从系统其他部分获取数据,或者进行耗时的计算。在这种情况下,可以通过单例模式防止多次创建相同的对象,节省系统资源。
  3. 保持全局一致性:存在一些全局状态需要持续维护,而状态的一致性对系统的运行至关重要。在这种情况下,单例模式可以帮助我们确保全局的一致性。
劣势
  1. 违背了"单一职责原则":单一职责原则意味着一个类应该只有一个引起它变化的原因。但在单例模式中,此类既管理对象的创建,又处理业务逻辑,因此当业务逻辑需要修改时,有可能对类的实例化部分造成影响。
  2. "隐藏"了类之间的依赖关系:使用单例模式可能会使得依赖关系不再明确,并可能会产生隐藏的依赖关系,这可能会导致代码维护和理解上的困难。
  3. 处理并发问题:虽然Java和Go等语言提供了处理并发问题的机制,但在其他不支持这些机制的语言中,需要特别注意并发环境下单例的创建和使用。
  4. 难以进行单元测试:由于单例模式在全局只有一个实例,因此当单例对象的状态发生改变时可能影响到其他的测试用例。

作为软件工程师,我们无疑会遇到各种各样关于编程范式和设计模式,包括开篇的单例模式在内的各种评论和观点。陈述有所不同,观点各异,这是必然的。然而,面对各种声音,我们并非要完全放弃使用单例模式。关键在于,我们需要根据特定的应用场景和特殊需求,实施审慎的判断和权衡。每一种设计模式都有其独特的用途和优势,只有明确了这一点,我们才能做出最适宜的决定,从而高效地解决遇到的问题。

工厂方法模式

工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个创建对象的接口,但将具体对象的创建延迟到子类中实现,以便根据不同条件创建不同类型的对象。

image.png

代码示例

让我们用一个餐馆制作食物的例子来阐释工厂方法模式:

假设你去了一个西餐厅,这个餐厅可以提供各种类型的食物,包括汉堡、比萨、三明治等。当你点单时,你并不知道厨师是如何烹饪这些食物的,你只需告诉服务员你的点单信息。

这里的餐厅相当于工厂,提供的食物相当于产品,点菜动作相当于调用工厂方法。

在代码中,我们可以将其抽象成接口,例如:

// gof/factory_method.go
package gof

import (
    "fmt"
)

// Food 接口定义了烹饪的共同行为
type Food interface {
    Cook() string
}

// Burger 是汉堡的具体类型
type Burger struct{}

func (b Burger) Cook() string {
    return "Cooking a burger."
}

// Pizza 是披萨的具体类型
type Pizza struct{}

func (p Pizza) Cook() string {
    return "Cooking a pizza."
}

// Kitchen 接口定义了厨房的行为
type Kitchen interface {
    CookFood(order string) Food
}

// Restaurant 实现了 Kitchen 接口
type Restaurant struct{}

func (r Restaurant) CookFood(order string) Food {
    switch order {
    case "burger":
        return &Burger{}
    case "pizza":
        return &Pizza{}
    default:
        fmt.Println("Guest ordered a food that doesn't exist.")
        return nil
    }
}

我们只要知道想吃何种食物,餐厅就会为顾客烹饪相应的食物,顾客并不需要知道烹饪食物的具体细节。

因此,餐厅烹饪食物的过程就是一个典型的工厂方法模式,它把制作食物的过程(创建产品的过程)和顾客消费食物的过程(使用产品的过程)进行了分离。

// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
    restaurant := &gof.Restaurant{}

    food1 := restaurant.CookFood("burger")
    fmt.Println(food1.Cook())

    food2 := restaurant.CookFood("pizza")
    fmt.Println(food2.Cook())

    food3 := restaurant.CookFood("sandwich")
    if food3 != nil {
        fmt.Println(food3.Cook()) // 无效的食物
    }
}

综合评估

类别 内容
优势
  1. 代码解耦:客户端代码不需要知道具体的产品类,只需要知道对应的工厂即可。这降低了客户端代码与具体产品类之间的耦合度。
  2. 代码可扩展性高:当增加新的产品时,只需要增加新的具体产品类和对应的工厂类,无需修改既有的代码,满足了“开放封闭原则”。
  3. 将对象创建集中管理,可以一定程度上控制创建对象的数量和申请的资源,例如连接池、线程池等。
劣势
  1. 代码复杂性增加:为每一个产品都要提供一个工厂类,如果产品数量非常多,会导致工厂类的数量也非常多,增加了代码的复杂性。
  2. 增加了系统的抽象性和理解难度,对于简单的对象创建,使用工厂方法可能会显得过于复杂。
  3. 当产品族中产品类增加的时候,所有的工厂类都需要进行修改,可能会引入错误。

总的来说,工厂方法模式在某些情景下能大大提升代码的可维护性和可扩展性,但是如若应用不当,也可能会导致代码复杂性增加。因此,是否使用工厂方法模式,需要根据具体情况进行选择。

抽象工厂模式

抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,它提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定具体的类,以实现对象的独立创建与使用。

image.png

代码示例

让我们再次使用餐馆的例子帮助理解升级版的抽象工厂模式:

假设你去了一个大型综合餐厅,这个餐厅既有西餐厅也有中餐厅,分别提供各类西餐和中餐。当你坐在餐桌旁,你不关心菜品是如何准备的,你只需要点单并享用。这里的各类餐厅就是抽象工厂,菜品就是产品。

在代码层面,你可以抽象出一个食品接口和一个餐厅接口:

// gof/abstract_factory.go
package gof

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

// 具体的食物:汉堡和披萨
type Burger struct{}

func (b Burger) Cook() string {
    return "Making a burger."
}

type Pizza struct{}

func (p Pizza) Cook() string {
    return "Making a pizza."
}

// 具体的食物:饺子和面条
type Dumpling struct{}

func (b Dumpling) Cook() string {
    return "Making dumplings."
}

type Noodle struct{}

func (p Noodle) Cook() string {
    return "Making noodles."
}

// 餐厅接口
type Restaurant interface {
    MakeMainDish() Food
    MakeSideDish() Food
}

// 西餐厅餐厅(抽象工厂)
type WesternRestaurant struct{}

func (w WesternRestaurant) MakeMainDish() Food {
    return &Burger{}
}
func (w WesternRestaurant) MakeSideDish() Food {
    return &Pizza{}
}

// 中餐厅餐厅(抽象工厂)
type ChineseRestaurant struct{}

func (c ChineseRestaurant) MakeMainDish() Food {
    return &Dumpling{}
}
func (c ChineseRestaurant) MakeSideDish() Food {
    return &Noodle{}
}

这样可以保证当你在中餐厅用餐时,你只能得到中餐,当你在西餐厅用餐时,你只能得到西餐。即,餐厅(抽象工厂)保证给你提供的始终是正确类别的食物(产品)。所以,无论你进入的是哪个类型的餐厅,你都会收到对的主菜和副菜,这就是抽象工厂模式的魅力所在。

// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
    chinese := &gof.ChineseRestaurant{}
    chineseFoodMain := chinese.MakeMainDish().Cook()
    chineseFoodSide := chinese.MakeSideDish().Cook()
    fmt.Println(chineseFoodMain, chineseFoodSide)

    western := &gof.WesternRestaurant{}
    westernFoodMain := western.MakeMainDish().Cook()
    westernFoodSide := western.MakeSideDish().Cook()
    fmt.Println(westernFoodMain, westernFoodSide)
}

综合评估

类别 内容
优点
  1. 分离接口和实现:客户端通过工厂接口创建需要的对象,而不需要关心对象的创建细节。
  2. 让更换产品族变得容易:由于抽象工厂类固定提供所有产品类,每个具体工厂类只需确定产品即可,便于系统扩展。
  3. 提高灵活性和抗变异性:抽象工厂模式提供的高度抽象性使得系统更具有灵活性和抗变异性,便于应对变动。
  4. 它避免了"具体产品"(也就是被创建的对象)与客户代码的耦合。
劣势
  1. 不太容易支持新种类的产品:如果需新增不属于现有的产品族,要修改的地方会非常多。
  2. 由于涉及到多个工厂类和产品类,所以代码的复杂性会提高。
  3. 采用了高级别的抽象,设计和理解起来可能较复杂。

抽象工厂模式就像是工厂方法模式的进一步延伸和泛化,它的优劣性也更加显著。使用抽象工厂模式是需要权衡的,它提供了很大的灵活性,而且可以提高代码的可复用性,但它也可能使你的代码更加复杂,更难以调试和维护。所以选择时需要判断当前的系统架构是否真的需要抽象工厂模式。

建造者模式

建造者模式(Builder Pattern)是一种创建型设计模式,它通过将对象的构建过程与表示分离,使得可以使用相同的构建过程来创建不同的表示,并且可以逐步构建复杂对象,也常称为 “生成器模式”。

image.png

代码示例

示例一

使用构建器模式创建手机对象的过程。MobilePhone结构体表示手机,具有一些属性和 getter 方法。MobilePhoneBuilder接口定义了设置手机属性和构建手机的方法,而PhoneBuilder结构体实现了该接口。

// gof/builder.go
package gof

import (
    "fmt"
)

// MobilePhone 表示手机结构体
type MobilePhone struct {
    account    string
    location   string
    language   string
    preference string
}

/*
    添加一些对外可暴露的 Getter 方法 和 String 方法
*/

// Account 返回手机账号
func (m *MobilePhone) Account() string {
    return m.account
}

// Location 返回手机地点
func (m *MobilePhone) Location() string {
    return m.location
}

// String 返回手机对象的字符串表示
func (m *MobilePhone) String() string {
    return fmt.Sprintf("[account=%s, location=%s, language=%s, preference=%s]",
        m.account, m.location, m.language, m.preference)
}

/*
    Builder 模式与 Setter 方法
*/

// MobilePhoneBuilder 是手机构建器接口,定义了设置手机属性和构建手机的方法
type MobilePhoneBuilder interface {
    SetAccount(string) MobilePhoneBuilder
    SetLocation(string) MobilePhoneBuilder
    SetLanguage(string) MobilePhoneBuilder
    SetPreference(string) MobilePhoneBuilder
    Build() *MobilePhone
}

// PhoneBuilder 是手机构建器结构体,实现了 MobilePhoneBuilder 接口
type PhoneBuilder struct {
    phone *MobilePhone
}

// NewPhoneBuilder 创建一个新的手机构建器
func NewPhoneBuilder() PhoneBuilder {
    return PhoneBuilder{phone: &MobilePhone{}}
}

// SetAccount 设置手机账号
func (p *PhoneBuilder) SetAccount(account string) MobilePhoneBuilder {
    p.phone.account = account
    return p
}

// SetLocation 设置手机地点
func (p *PhoneBuilder) SetLocation(location string) MobilePhoneBuilder {
    p.phone.location = location
    return p
}

// SetLanguage 设置手机语言
func (p *PhoneBuilder) SetLanguage(language string) MobilePhoneBuilder {
    p.phone.language = language
    return p
}

// SetPreference 设置手机偏好
func (p *PhoneBuilder) SetPreference(preference string) MobilePhoneBuilder {
    p.phone.preference = preference
    return p
}

// Build 构建手机对象
func (p *PhoneBuilder) Build() *MobilePhone {
    return p.phone
}

main 包中,我们创建了一个手机初始化构建器,并使用链式调用设置了手机的账号、地点、语言和偏好。然后,通过调用 Build() 方法来构建手机对象。

注意这四个属性的设置顺序没有关联,可以随意调换,比如,我们可以先设置语言,再设置账户,然后设置位置,最后设置偏好,也可以按照任何其他顺序进行。如果你需要设置的属性没有固定顺序,你可以使用建造者模式中的 Set 方法来设置你需要的属性并得到最终对象。

// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
 // 创建手机初始化构建器
    builder := gof.NewPhoneBuilder()

    // 使用链式调用设置手机属性并构建手机对象
    phone := builder.SetAccount("user@example.com").
        SetLocation("China").
        SetLanguage("Chinese").
        SetPreference("dark mode").Build()

    // 打印手机暴露出来的属性
    fmt.Printf("Account: %s\n", phone.Account())
    fmt.Printf("Location: %s\n", phone.Location())
    fmt.Printf("Mobile phone initialization displays: %s\n", phone)
}

示例二

这段代码展示了一个手机制造流程的示例,其中包含了使用建造者模式构建流程对象的方法。

手机制造过程包括六个步骤,这六个步骤有明确的顺序关系,比如我们需要先进行市场调研和规划,然后进行设计和工程开发,接着是零部件采购,之后是生产装配和质量控制,然后是软件开发和优化,最后是销售和营销。这些步骤按照一定顺序进行,互有依赖,中间不能省略任何步骤。

// gof/builder.go
package gof

import (
    "fmt"
    "strings"
)

// Procedure 结构体表示流程
type Procedure struct {
    steps []string
}

// AddStep 向流程中添加步骤
func (p *Procedure) AddStep(step string) {
    p.steps = append(p.steps, step)
}

// Execute 执行流程并返回结果字符串
func (p *Procedure) Execute() string {
    var sb strings.Builder

    for i, step := range p.steps {
        sb.WriteString(fmt.Sprintf("%d. %s;\n", i+1, step))
    }

    return sb.String()
}

// PhoneManufactureProcedureBuilder 是手机制造流程构建器接口,定义了设置各个步骤和构建流程的方法
type PhoneManufactureProcedureBuilder interface {
    MarketResearch() PhoneManufactureProcedureBuilder
    DesignDevelopment() PhoneManufactureProcedureBuilder
    ComponentPurchase() PhoneManufactureProcedureBuilder
    AssemblyQualityControl() PhoneManufactureProcedureBuilder
    SoftwareDevelopment() PhoneManufactureProcedureBuilder
    SalesMarketing() PhoneManufactureProcedureBuilder
    Build() *Procedure
}

// PhoneBuilder 是手机制造流程构建器结构体,实现了 PhoneManufactureProcedureBuilder 接口
type PhoneBuilder struct {
    procedure *Procedure
}

// NewPhoneBuilder 创建一个新的手机制造流程构建器
func NewPhoneBuilder() *PhoneBuilder {
    return &PhoneBuilder{procedure: &Procedure{}}
}

// MarketResearch 市场调研步骤
func (b *PhoneBuilder) MarketResearch() PhoneManufactureProcedureBuilder {
    b.procedure.AddStep("Carry out market research and needs analysis")
    return b
}

// DesignDevelopment 设计开发步骤
func (b *PhoneBuilder) DesignDevelopment() PhoneManufactureProcedureBuilder {
    b.procedure.AddStep("Proceed with design and engineering development")
    return b
}

// ComponentPurchase 组件采购步骤
func (b *PhoneBuilder) ComponentPurchase() PhoneManufactureProcedureBuilder {
    b.procedure.AddStep("Purchase components and manage supply chain")
    return b
}

// AssemblyQualityControl 装配质量控制步骤
func (b *PhoneBuilder) AssemblyQualityControl() PhoneManufactureProcedureBuilder {
    b.procedure.AddStep("Start production assembly and quality control")
    return b
}

// SoftwareDevelopment 软件开发步骤
func (b *PhoneBuilder) SoftwareDevelopment() PhoneManufactureProcedureBuilder {
    b.procedure.AddStep("Develop and optimize software")
    return b
}

// SalesMarketing 销售营销步骤
func (b *PhoneBuilder) SalesMarketing() PhoneManufactureProcedureBuilder {
    b.procedure.AddStep("Start sales and marketing")
    return b
}

// Build 构建手机制造流程
func (b *PhoneBuilder) Build() *Procedure {
    return b.procedure
}

// StandardManufacturingProcess 锁定/固定的标准制造流程
func StandardManufacturingProcess() string {
    builder := NewPhoneBuilder()
    procedure := builder.MarketResearch().DesignDevelopment().ComponentPurchase().AssemblyQualityControl().SoftwareDevelopment().SalesMarketing().Build()
    return procedure.Execute()
}
// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
    mfgPhone := gof.StandardManufacturingProcess()
    fmt.Println(mfgPhone)
}

总结起来,建造者模式非常灵活,可以处理各种不同的情况。如果你的构建过程有明确的顺序,并且中间不能省略任何步骤,建造者模式可以帮助你按照预定顺序执行所有步骤。如果你需要设置的属性没有固定顺序,你可以使用建造者模式中的 Setter 方法来设置你需要的属性并得到最终对象。

实际上,上述这两种方式的核心都是建造者模式,即通过一个封装了创建和组装复杂对象各个部分的建造者来创建对象。第一种更接近于教科书或标准库中的建造者模式实现,这种方式提供了一种流畅的方式来设置对象的各种属性,并且这些属性的设置顺序通常无关紧要。

而第二种方式有时候也是有用的,尤其是对于一些复杂的创建过程,这些过程有固定的步骤顺序,每个步骤可能会影响到下一步。通过添加每个步骤到一个链(这里就是 Proceduresteps 切片),我们可以保持并执行这些步骤。

总之,两种方式都是为了简化创建和初始化对象的过程,为了达到这样的目标,我们需要结合具体需求和上下文来决定采用何种构建者的变体方式。

综合评估

类别 内容
优点
  1. 可读性:Builder 模式透过清晰的方法名称操作对象,使得代码更具有可读性。
  2. 强制性:当创建对象需要很多步骤,并且有些步骤是强制的,Builder模式能够强制执行这些步骤。
  3. 分离对象创建过程和对象本身:Builder 模式可以使我们的对象构造逻辑与对象本身分离,让代码更具有可扩展性。这也遵循了单一职责原则。
劣势
  1. 创建额外的 Builder 类:使用 Builder 模式需要单独创建 Builder 类。如果创建的对象结构复杂,那么 Builder 类的代码就会变得很复杂。
  2. 额外开销:对于简单的对象来说,直接使用传统的构造方法可能更加简单快速,Builder 模式在这种情况下可能引入额外的开销。
  3. 对象修改难度:如果一个对象在构造后需要做一定的修改,Builder 模式可能并不方便实现这一点,因为 Builder 模式主要是用于创建不可变的对象。

尽管我们已经列出了 Builder 模式的一些劣势,但实际上,这种模式仍被广泛采用,并且在许多情况下都表现出了巨大的优势。OkHttp 库,是 JAVA 中使用 Builder 模式的一个很好的例子,通过使用 Builder 模式,它成功地实现了构建复杂的 HTTP 请求的功能。还是要记住,设计模式只是工具,至于选择使用哪种工具应根据项目的具体需求进行决定。

原型模式

原型模式(Prototype Pattern)是一种创建型设计模式,它通过复制现有的对象实例来创建新的对象,而不是通过新实例化的方式,也常称为 "Clone" 模式。

image.png

代码示例

定义了Cloneable 接口,该接口只有一个 Clone 方法,返回的也是 Cloneable 接口。

// gof/prototype.go  
package gof  

// Cloneable 是一个定义了 Clone 方法的接口  
type Cloneable interface {
    Clone() Cloneable
}

我们有一个 Business 结构体,表示程序中的一个业务对象,它有一系列字段,包括值类型和引用类型。这里代表业务的测试数据。

// service/business.go
package service

// Business 某个业务结构体
type Business struct {
    Name   string
    Age    int64
    Gender bool

    DataShallowMap   map[string]interface{}
    DataDeepArray    []string
    DataShallowArray *[]string
}

// NewBusiness 实例化一些测试数据
func NewBusiness() *Business {
    list := []string{"a", "b", "c"}
    return &Business{
        Name:   "Original",
        Age:    18,
        Gender: true,
        DataShallowMap: map[string]interface{}{
            "a": 1,
            "b": 2,
            "c": map[string]interface{}{
                "c1": 1,
                "c2": 2,
            },
        },
        DataDeepArray:    list,
        DataShallowArray: &list,
    }
}

main 包中,我们首先从 service 包中获取一个 Business 实例,然后我们创建了一个 Prototype 结构体,它包含了一个指向 Business 的指针,实现了在 gof 包中定义的 Cloneable 接口。Clone 方法创建了一个 Business 结构体的浅拷贝:它复制了所有值类型的字段,而对于引用类型的字段,它复制了它们的引用而不是它们的值。然后,我们使用这个方法来复制 Prototype 实例,并在 protoclone 实例上做了一些更改。至此,你可以清楚地看到,在这些更改后,原始对象和克隆对象是如何保持它们的独立性的。

// main.go
package main

import (
    "fmt"

    "codebase/gof"
    "codebase/service"
)

/*
    我们下面来实现 Cloneable 接口
*/

// Prototype 是一个要被拷贝的结构体
type Prototype struct {
    b *service.Business
}

// Clone 创建并返回 Prototype 的一个副本
func (p *Prototype) Clone() gof.Cloneable {
    // 复制 map
    dataShallowMap := make(map[string]interface{})
    for k, v := range p.b.DataShallowMap {
        dataShallowMap[k] = v
    }

    // 复制切片
    dataDeep := make([]string, len(p.b.DataDeepArray))
    copy(dataDeep, p.b.DataDeepArray)

    // 复制指针
    dataShallow := p.b.DataShallowArray

    return &Prototype{
        b: &service.Business{
            Name:             p.b.Name,
            Age:              p.b.Age,
            Gender:           p.b.Gender,
            DataShallowMap:   dataShallowMap,
            DataDeepArray:    dataDeep,
            DataShallowArray: dataShallow,
        },
    }
}

func main() {
    // 比如在业务中拿到一些数据,我们需要拿到结果,并做进一步处理,
    // 然而,又不能更改原始数据,我们就可以使用 Prototype 模式来 copy 一份
    business := service.NewBusiness()

    // 创建一个 Prototype 实例
    proto := &Prototype{
        b: business,
    }

    // 复制 Prototype
    clone := proto.Clone().(*Prototype)

    // 修改原 Proto
    proto.b.Name = "Modified"
    proto.b.Age = 30
    proto.b.Gender = false
    proto.b.DataShallowMap["a"] = "Modified" // 边注[1]
    proto.b.DataShallowMap["c"].(map[string]interface{})["c1"] = "Stain"
    proto.b.DataDeepArray = append(proto.b.DataDeepArray, "Modified")
    *proto.b.DataShallowArray = append(*proto.b.DataShallowArray, "Stain")

    // 打印指针地址和值
    fmt.Printf("proto pointer: %p, value: { Name: %s, Age: %d, Gender: %v, DataShallowMap: %+v, DataDeepArray: %+v, DataShallowArray: %+v }\n",
       proto, proto.b.Name, proto.b.Age, proto.b.Gender, proto.b.DataShallowMap, proto.b.DataDeepArray, *proto.b.DataShallowArray)
    fmt.Printf("clone pointer: %p, value: { Name: %s, Age: %d, Gender: %v, DataShallowMap: %+v, DataDeepArray: %+v, DataShallowArray: %+v }\n",
       clone, clone.b.Name, clone.b.Age, clone.b.Gender, clone.b.DataShallowMap, clone.b.DataDeepArray, *clone.b.DataShallowArray)
}

这个程序展示了如何使用原型模式来复制一个包含各种不同类型字段的复杂对象,同时又能保持原始对象和复制的对象的独立性。这种方式在许多实际编程场景中非常有用,比如你需要复制一个对象,但又不能(或不希望)影响到原始对象时,原型设计模式就是一个很好的解决方案。

*边注[1]

通过运行程序,已经能够明确地观察到,对于 map 的首层 kv,确实是通过深拷贝创建了新的副本。也就是说,新的 map 创建了独立的存储空间,原始 map 中 kv 的修改不会对新的 map 产生影响。但是,当 map 的值是引用类型的数据(例如,另一个 map、切片或指针),这个新创建的 map 的值并非原 map 值自身的拷贝,而仅仅是它们的引用的复制。因此,我们普遍会认为这是“浅拷贝”。

然而,就 for 循环遍历并将第一层的 map 的 kv 重新赋值到新创建(make)的 map 的过程来看,从行为本身来看,的确类似于“深拷贝”。但如果从最终效果来看,把它称为“浅拷贝”也无可厚非,这主要是它的某个 kv 的 v 的类型在作怪。

因此,对于我们来说,最重要的是解决实际问题,如果只需对第一层进行“深拷贝”就已足够,那么我们称这个操作为“深拷贝”也并非不可接受。这个说法将取决于你特定的需求以及你正在尝试解决的问题。而并不是 “xx 拷贝” 这个术语词汇本身,并不具备太大的探讨意义。

综合评估

类别 内容
优势
  1. 高效:如果对象的创建成本较高,原型模式允许你复制现有实例,这比重新构造一个实例更加高效。
  2. 简化对象创建:当待创建的对象与现有的对象非常相似时,可以通过复制和少量修改来简化对象的创建过程。
  3. 在运行时动态添加和删除对象。
  4. 通过简单的复制操作实现复杂对象的快速创建。
劣势
  1. 复杂的实现:当对象的内部包含其他对象或引用类型时,需要实现深拷贝。
  2. 克隆的对象和原始对象之间可能存在关联性。
  3. 不适合一些场景:如果创建对象的过程涉及到外部资源或系统状态。
  4. 需要考虑对象复制后的内存占用。

结构型模式

适配器模式

适配器模式(Adapter Pattern)是一种结构性设计模式,它允许将一个类的接口转换成客户端所期望的另一个接口,以解决不兼容接口之间的互操作问题,也常称为 “封装器” 模式。

image.png

代码示例

假设你要去美国旅行。在中国,你的充电器是按照 220V 的电源标准制作的。但是在美国,电源标准是 110V。直接用你的中国充电器插在美国的插座里,显然是不行的。

为解决这个问题,你在某多多平台上找到了一个间接的解决方案:一个电源转换头。你发现有一个叫做 USAAdapter 的适配器,它可以接受一个 ChinesePlug,并把输出的电压转换为美国的标准。

// main.go
package main

import (
    "fmt"
)

// 中国的电源插头
type ChinesePlug struct{}

func (p ChinesePlug) OutputInChinaStandard() int {
    // Output power with Chinese standard
    return 220
}

// 中国转美国的电源适配器
type USAAdapter struct {
    Plug ChinesePlug
}

func (a USAAdapter) OutputInUSAStandard() int {
    // 定义一个内部函数,负责核心转换即电压转换
    coreConversion := func(power int) int {
        // Convert the power from 220v to 110v
        return power / 2
    }

    // 获取中国标准的电压
    power := a.Plug.OutputInChinaStandard()

    // 使用定义的内部函数来转换电压标准
    return coreConversion(power)
}

func main() {
    // 这样中国的电源插头接上美国的转换器就可以用了,不用特意买美国的插头了
    plug := ChinesePlug{}
    adapter := USAAdapter{Plug: plug}
    fmt.Println(adapter.OutputInUSAStandard())
}

综合评估

类别 内容
优势
  1. 接口兼容:适配器模式可以让原本接口不兼容的两个类能够一起工作,提高代码的灵活性和可用性。
  2. 代码复用:适配器模式可以复用现有的类,其转换接口的能力使得原本不适用的类可以被再次利用。
  3. 松耦合:适配器模式可以提供一种松耦合的设计,让代码在修改时更灵活,更易于进行单元测试。
劣势
  1. 复杂性增加:在 Go 语言中,有时可能需要大量的代码去定义和实现一个适配器类型。
  2. 可读性降低:过度使用适配器模式可能会导致系统复杂度增加,同时代码的可读性降低。
  3. 代码冗余:适配器可能会使得系统中存在大量的相似代码,这可能会给代码的维护和管理带来困难。

在实际的项目开发中,适配器模式有着广泛的应用。主要用途可以归纳为以下几点:

  1. 接口转换:如果你正在开发一个库或者模块,并打算将其作为公共服务供其他系统使用,你肯定希望你的接口可以尽可能通用,以便适应各种使用场景。然而,如果其他系统已经有一些预定的接口,并且他们希望使用你的模块,这时,你可能就需要提供一个适配器来翻译你的接口,使其适应其他系统的需求。
  2. 旧代码复用:如果你正在改造一个大型的旧系统,存在许许多多的旧代码需要复用,这时可能就需要适配器来协助你完成任务。你可以写一个适配器来包装这些旧代码,使得它们看起来就像是新代码提供的接口一样。
  3. 软件中间件开发:如果你正在开发类似RPC框架、消息中间件这样的系统,其中大量的数据编解码,协议转换等工作都需要通过适配器模式来完成。
  4. 跨语言或跨平台的开发:如果你的系统需要面对多种语言或多种平台,适配器模式也是必不可少的设计模式之一。

通常情况下,适配器模式都是用来解决 "已有的系统无法直接使用,但又不能对其进行修改" 这类问题。

桥接模式

桥接模式(Bridge Pattern)是一种结构性设计模式,它将抽象部分与实现部分解耦,使它们可以独立变化,以实现更灵活的系统设计。

image.png

代码示例

我们购买了两款游戏 "堡垒之夜" 和 "我的世界"。无论是在个人 PC 上,还是在桌面游戏机 PS5,甚至是在掌机 Switch 上,你都能找到这两款游戏的身影。

为了准确地描述这两款游戏和各种平台的关系,我们设计了 "Game" 和 "Platform" 这两个接口。"Game" 接口有一个 "Play" 方法,用来模拟游戏的运行,而 "Platform" 接口有一个 "Run" 方法,它接收一个 "Game" 参数,用来模拟游戏在特定平台上的运行。

然后我们针对这两款游戏,以及各个平台,分别创建了对应的结构体,并实现了 "Game" 和 "Platform" 接口。

有了以上的准备,我们就可以轻松地在任意的平台上运行任意的游戏了。同时,如果未来有新的游戏或新的平台出现,我们也只需要简单地添加新的结构体并实现对应的接口即可,无需修改现有的代码。这就是桥接模式的强大之处,它可以最大程度上降低不同部分之间的耦合度,使得各部分可以独立地进行变化。

// gof/bridge.go
package gof

// 定义游戏和平台的接口
type Game interface {
    Play() string
}

type Platform interface {
    Run(game Game) string
}

// 定义具体的游戏,包括 "堡垒之夜" 和 "我的世界"
type Fortnite struct{}

func (f Fortnite) Play() string {
    return "Playing Fortnite"
}

type Minecraft struct{}

func (m Minecraft) Play() string {
    return "Playing Minecraft"
}

// 为每个平台定义具体的类型
type PC struct{}

func (p PC) Run(game Game) string {
    return "On PC: " + game.Play()
}

type PS5 struct{}

func (p PS5) Run(game Game) string {
    return "On PS5: " + game.Play()
}

type Switch struct{}

func (s Switch) Run(game Game) string {
    return "On Switch: " + game.Play()
}
// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

// 在各个平台上运行游戏
func main() {
    fortnite := gof.Fortnite{}
    minecraft := gof.Minecraft{}

    pc := gof.PC{}
    fmt.Println(pc.Run(fortnite))
    fmt.Println(pc.Run(minecraft))

    ps5 := gof.PS5{}
    fmt.Println(ps5.Run(fortnite))
    fmt.Println(ps5.Run(minecraft))

    nSwitch := gof.Switch{}
    fmt.Println(nSwitch.Run(fortnite))
    fmt.Println(nSwitch.Run(minecraft))
}

综合评估

类别 内容
优势
  1. 分离抽象和实现:使得两者可以独立地变化,扩展能力强。
  2. 提高可扩展性:抽象和实现可以单独扩展,不会相互影响。
  3. 实现细节对客户透明:对用户隐藏实现细节,降低系统复杂。
  4. 满足单一职责原则:满足高内聚,低耦合的设计。
劣势
  1. 增加了系统的理解和设计难度:桥接模式的引入会增加系统的理解和设计的复杂性。
  2. 识别出两个独立变化的维度较难:应用桥接模式需要正确识别出两个独立变化的维度,对于一些场景可能较难实现。

总的来说,桥接模式是一种非常强大的设计模式,主要用于将抽象部分与其实现部分进行分离,使得两者可以独立地进行变化。在许多实际开发场景中,如支持多平台,多界面,多算法等方面都有其独特的应用。

组合模式

组合模式(Composite Pattern)是一种结构性设计模式,它将对象组合成树状结构,以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性,也常称为 "对象树" 模式。

image.png

代码示例

下面这个例子演示了如何使用组合设计模式来模拟一个类 Unix 文件系统的树状结构。为了表示文件系统中的两种主要类型的节点 - 文件(File)和目录(Directory),我们定义了一个名为 FileSystemNode 的接口,它有两个方法:

  • Name() 用来返回该节点的名称。
  • Print(prefix string, last bool) 用来输出该节点以及它的子节点。

File 类型只包含一个名称属性,并在 Print 方法中简单地输出其名字。因为它是文件系统树中的叶子节点,所以它没有子节点。

Directory 类型既包含名称属性,也包含一个 children 切片来保存它的子节点。这些子节点可以是 DirectoryFile 类型。它实现了 Add 方法来添加子节点。在 Print 方法中, Directory 类型先输出自己的名字,然后循环遍历自己的所有子节点调用它们的 Print 方法,形成递归的输出。

main 函数中,我们模拟了一个类Unix结构的文件系统,模拟的组成部分主要包括 /, /etc, /home, /opt, /tmp, /usr, /var 等目录和一些文件。最终效果参考 Linuxtree 命令。

// main.go
package main

import (
    "fmt"
)

// FileSystemNode 是一个接口,表示文件系统中的节点,
// 包括文件(File)和文件夹(Directory)。
type FileSystemNode interface {
    Name() string                   // 返回节点的名称
    Print(prefix string, last bool) // 以tree格式打印文件系统的层级结构
}

// File 结构体实现了 FileSystemNode 接口,代表文件节点
type File struct {
    name string
}

func (f *File) Name() string {
    return f.name
}

// File 的 Print 方法会打印文件的名字,注意这里有以 "├──" 或 "└──" 开头的格式选择,
// 这是为了让输出的形式更接近 Linux 的 `tree` 命令的输出。
func (f *File) Print(prefix string, last bool) {
    if last {
        fmt.Printf("%s└──%s\n", prefix, f.name)
        return
    }
    fmt.Printf("%s├──%s\n", prefix, f.name)
}

// Directory 结构体实现了 FileSystemNode 接口,代表目录节点。
// Directory 同时包含了一个 FileSystemNode 类型的切片,用来存放它的子节点。
type Directory struct {
    name     string
    children []FileSystemNode
}

func (d *Directory) Name() string {
    return d.name
}

// Add 方法允许向一个文件夹添加子节点。
func (d *Directory) Add(node FileSystemNode) {
    d.children = append(d.children, node)
}

// Directory 的 Print 方法首先会打印文件夹自己的名字,
// 然后检查如果还有子节点,会递归调用每个子节点的 Print 方法,
// 为了正确的打印格式,方法会根据当前节点是否为最后一个节点,传入不同的前缀。
func (d *Directory) Print(prefix string, last bool) {
    if last {
        fmt.Printf("%s└──%s\n", prefix, d.name)
        prefix += "   "
    } else {
        fmt.Printf("%s├──%s\n", prefix, d.name)
        prefix += "│  "
    }

    for i := 0; i < len(d.children); i++ {
        last := i == len(d.children)-1
        d.children[i].Print(prefix, last)
    }
}

func main() {
    // 根目录 /
    rootDir := &Directory{name: "/"}

    // 创建 /etc 目录并添加子节点
    etcDir := &Directory{name: "etc/"}
    etcDir.Add(&File{name: "fstab"})
    etcDir.Add(&File{name: "hosts"})
    nginxDir := &Directory{name: "nginx/"}
    nginxConfFile := &File{name: "nginx.conf"}
    nginxDir.Add(nginxConfFile)
    etcDir.Add(nginxDir)
    etcDir.Add(&File{name: "passwd"})
    etcDir.Add(&File{name: "profile"})
    sshDir := &Directory{name: "ssh/"}
    sshConfFile := &File{name: "sshd_config"}
    sshDir.Add(sshConfFile)
    etcDir.Add(sshDir)
    rootDir.Add(etcDir)

    // 创建 /home 目录并添加子节点
    homeDir := &Directory{name: "home/"}
    userDir := &Directory{name: "username/"}
    bashrcFile := &File{name: ".bashrc"}
    userDir.Add(bashrcFile)
    homeDir.Add(userDir)
    rootDir.Add(homeDir)

    // 创建 /opt 目录
    optDir := &Directory{name: "opt/"}
    rootDir.Add(optDir)

    // 创建 /tmp 目录并添加子节点
    tmpDir := &Directory{name: "tmp/"}
    tempFile := &File{name: "temp.txt"}
    tmpDir.Add(tempFile)
    rootDir.Add(tmpDir)

    // 创建 /usr 目录并添加子节点
    usrDir := &Directory{name: "usr/"}
    binDir := &Directory{name: "bin/"}
    catFile := &File{name: "cat"}
    lsFile := &File{name: "ls"}
    pwdFile := &File{name: "pwd"}
    binDir.Add(lsFile)
    binDir.Add(pwdFile)
    binDir.Add(catFile)
    usrDir.Add(binDir)
    localDir := &Directory{name: "local/"}
    usrDir.Add(localDir)
    rootDir.Add(usrDir)

    // 创建 /var 目录并添加子节点
    varDir := &Directory{name: "var/"}
    logDir := &Directory{name: "log"}
    varDir.Add(logDir)
    rootDir.Add(varDir)

    // 打印整个文件系统的目录结构
    rootDir.Print("", true)
}

参考输出:

└──/
   ├──etc/
   │  ├──fstab
   │  ├──hosts
   │  ├──nginx/
   │  │  └──nginx.conf
   │  ├──passwd
   │  ├──profile
   │  └──ssh/
   │     └──sshd_config
   ├──home/
   │  └──username/
   │     └──.bashrc
   ├──opt/
   ├──tmp/
   │  └──temp.txt
   ├──usr/
   │  ├──bin/
   │  │  ├──ls
   │  │  ├──pwd
   │  │  └──cat
   │  └──local/
   └──var/
      └──log

综合评估

类别 内容
优势
  1. 可以清楚地定义分层次的复杂对象,比如整个目录结构。
  2. 容易添加新类型的组件。我们只需确定新类型组件与其他组件的相同接口及其实现。
  3. 使客户端将复合和单个对象统一看待。它们具有相同的接口,可以通过相同的方式进行处理。
劣势
  1. 设计会更加复杂。我们需要仔细控制和操作对象之间的关系。
  2. 不容易限制组件的类型。特定组件可能只能添加某些特定的子组件,而这在接口中是无法强制的。

组合模式被广泛应用在诸多需要表现树形结构的场景中,除了前述的目录文件系统之外,还有许多其他的应用场景。例如:

  • 动态路由:可以模拟出多级路由的嵌套情况,实现复杂的路由管理。
  • 折叠菜单栏:应用组合模式,可以轻松实现多级菜单的创建和管理,例如网站的导航栏、侧边栏等。
  • 组织架构图:通过组合模式,你可以分层次地处理像部门和子部门这种具有树形结构的组织关系。
  • GUI组件:组合模式也常用来管理图形用户界面的层次结构,如按钮组,面板等。

此外,任何我们可以归类的树形结构事物,都能利用组合模式来实现。例如,在购物车中的商品(可能包含其他小商品的商品组合,或者像产品套装这样的复合商品),操作系统的权限管理(包含用户角色的嵌套分组)等都是非常适合使用组合模式来处理的实例。

装饰器模式

装饰器模式(Decorator Pattern)是一种结构性设计模式,它动态地给对象添加额外的职责,即不改变其接口的情况下,通过装饰类包装原始对象,实现对对象功能的扩展,也常称为 "Wrapper"。

image.png

代码示例

从某种意义上讲,装饰器模式有点像在处理某个事件的 “前后” 添加一些额外的行为,就如同你在吃饭时,可以选择在正餐前吃一些开胃菜,或在餐后吃点甜点一样。

装饰器模式通常用于在不修改已有函数或方法的前提下,为其添加新的功能或行为。这个"添加的行为"就好比餐前的开胃菜和餐后的甜点,而"正餐"则类似于那个被装饰的函数或方法本体。

此外,也可以把装饰器看作是一种特殊类型的 “中间件” 或 “拦截器”。在某些框架中,中间件或拦截器的作用就是在请求处理的前后执行一些操作,例如记录日志、权限检查、数据格式转换等,这个概念基本上相当于装饰器。

示例一

下面是一个在 Go 语言中使用装饰器模式的最常见的例子。Go 语言并没有像 Python 那样对装饰器有原生语法支持的 @语法糖,但我们可以通过 Go 的函数式特性来模拟装饰器。这个例子中,我们将创建一个装饰器来计算函数的执行时间。

// main.go
package main

import (
    "fmt"
    "time"
)

// 我们定义一个函数类型 F,并且这个类型实现了接口 A 的方法,然后在这个方法中调用自己。
// 这是 Go 语言中装饰器的常见形式。
type F func(string)

func (f F) Decorate(s string) {
    fmt.Println("Started")
    start := time.Now()

    f(s) // 原始被装饰的函数

    fmt.Println("Completed")
    fmt.Println("Time Elapsed:", time.Since(start))
}

// 我们将为这个普通函数加以装饰
func Echo(s string) {
    fmt.Println(s)
}

func main() {
    var f F = Echo                  // 此处我们把 Echo 函数转换为 F 类型
    f.Decorate("Hello, Decorator!") // 使用 f.Decorate 对 Echo 进行装饰
}

示例二

下面这个例子,也使用 Go 实现的装饰器模式,体现了这种模式在实现仿 HTTP 请求处理管道(如日志记录、错误处理、认证等)时的应用。通过为核心业务逻辑(本例中为 Business 函数)添加多个中间层(装饰器),可以让我们的应用在执行业务逻辑前后进行一系列的附加处理,而无需更改业务逻辑本身。这种方式提高了代码的可扩展性和可维护性,并保留了对原始业务逻辑的完整性和清晰度。

为了实现我们的 “请求响应拦截器” 的代码需求,我们会使用到 “双层返回” 的结构,它允许你稍后传入一个函数并对其进行装饰,这样可以在 Go 中编写更加灵活的装饰器。对于从未尝试这样编写过的同学来说,这个示例理解起来可能会感觉很复杂,甚至有一些 BT 的地步。这很正常,但当你开始理解这种设计模式的工作方式及其潜力时,你就会开始欣赏它为你的代码带来的简洁和强大。

// gof/decorator.go
package gof

import (
    "context"
    "fmt"
)

// 定义上下文中的键,不要硬编码
const (
    CtxKey = "request"
)

// HandlerFunc 类型是一个函数,该函数接收一个 context.Context 类型的参数,这个 context.Context 会被用来在函数链中传递数据和信号
type HandlerFunc func(ctx context.Context)

// RequestContext 是我们为请求和响应信息定义的结构体,它会被注入到 context.Context 中
type RequestContext struct {
    Request     string
    Response    string
    MethodOrder string
}

// Decorator 用作装饰器函数,它接受一个 HandlerFunc 和序号索引,为 HandlerFunc 的处理提供前置和后置处理逻辑
// 在返回的函数中,我们可以添加新的行为,比如修改数据或者打印日志
type Decorator func(HandlerFunc, int) HandlerFunc

// ResponseDecorator 函数返回一个 Decorator,它对 HandlerFunc 的处理结果进行进一步包装
func ResponseDecorator() Decorator {
    return func(h HandlerFunc, index int) HandlerFunc {
        return func(ctx context.Context) {
            h(ctx)
            reqContext := ctx.Value(CtxKey).(*RequestContext)
            reqContext.Response = reqContext.Request + " -> OriginalResponse"
            // 记录装饰器的执行顺序
            reqContext.MethodOrder += fmt.Sprintf("ResponseDecorator Index:%d -> ", index)
        }
    }
}

// NewContext 函数是用于创建并返回一个携带 RequestContext 的 context.Context
func NewContext() context.Context {
    reqContext := &RequestContext{Request: "OriginalRequest"}
    ctx := context.Background()
    return context.WithValue(ctx, CtxKey, reqContext)
}

// WithMiddleware 函数接受一系列的 Decorator,返回了一个函数,该函数可以将所有的装饰器应用到 HandlerFunc 上
func WithMiddleware(decorators ...Decorator) func(HandlerFunc) HandlerFunc {
    return func(handler HandlerFunc) HandlerFunc {
        // 默认包含ResponseDecorator
        handler = ResponseDecorator()(handler, 0)

        // 遍历其他自定义装饰器
        // 反转装饰器数组使得第一个装饰器最先执行!!!
        for i := len(decorators) - 1; i >= 0; i-- { // 边注[1]
            handler = decorators[i](handler, i+1)
        }

        return handler
    }
}
// main.go
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "codebase/gof"
)

// CustomInterceptor 自定义的请求和响应的拦截器
func CustomInterceptor(desc string) gof.Decorator {
    return func(h gof.HandlerFunc, index int) gof.HandlerFunc {
        return func(ctx context.Context) {
            time.Sleep(1 * time.Second)
            log.Printf("Before handler: Executing '%s' request interceptor %d\n", desc, index)
            reqContext := ctx.Value("request").(*gof.RequestContext)
            reqContext.Request += fmt.Sprintf(" -> RequestInterceptor%d", index)

            defer func() {
                time.Sleep(1 * time.Second)
                log.Printf("After handler: Executing '%s' response interceptor %d\n", desc, index)
                reqContext.Response += fmt.Sprintf(" -> ResponseInterceptor%d", index)
            }()

            h(ctx)
        }
    }
}

// Business 需要执行的业务逻辑
func Business(ctx context.Context) {
    fmt.Printf("\nHandler Start")
    for i := 0; i < 5; i++ {
        time.Sleep(1 * time.Second)
        fmt.Printf(".")
    }
    fmt.Printf("Done!\n\n")
}

func main() {
    // 创建一个新的上下文
    ctx := gof.NewContext()

    // 生成中间件处理函数,包含了一系列的自定义拦截器
    mdHandle := gof.WithMiddleware(
        CustomInterceptor("Panic recovery"), // 边注[2]
        CustomInterceptor("Logging"),
        CustomInterceptor("Authorization"),
        CustomInterceptor("Cross-Origin Resource Sharing"),
        CustomInterceptor("Rate limiting"),
        CustomInterceptor("Session management"),
    )

    // 生成最终的处理函数,这个处理函数已经被一系列的拦截器装饰过
    handler := mdHandle(Business)

    // 执行这个处理函数
    handler(ctx)

    // 打印处理函数执行后的响应
    fmt.Println("---")
    fmt.Println(ctx.Value(gof.CtxKey).(*gof.RequestContext).Response)
}

参考输出:

2023/12/31 18:48:52 Before handler: Executing 'Panic recovery' request interceptor 1
2023/12/31 18:48:53 Before handler: Executing 'Logging' request interceptor 2
2023/12/31 18:48:54 Before handler: Executing 'Authorization' request interceptor 3
2023/12/31 18:48:55 Before handler: Executing 'Cross-Origin Resource Sharing' request interceptor 4
2023/12/31 18:48:56 Before handler: Executing 'Rate limiting' request interceptor 5
2023/12/31 18:48:57 Before handler: Executing 'Session management' request interceptor 6

Handler Start.....Done!

2023/12/31 18:49:03 After handler: Executing 'Session management' response interceptor 6
2023/12/31 18:49:04 After handler: Executing 'Rate limiting' response interceptor 5
2023/12/31 18:49:05 After handler: Executing 'Cross-Origin Resource Sharing' response interceptor 4
2023/12/31 18:49:06 After handler: Executing 'Authorization' response interceptor 3
2023/12/31 18:49:07 After handler: Executing 'Logging' response interceptor 2
2023/12/31 18:49:08 After handler: Executing 'Panic recovery' response interceptor 1
---
OriginalRequest -> RequestInterceptor1 -> RequestInterceptor2 -> RequestInterceptor3 -> RequestInterceptor4 -> RequestInterceptor5 -> RequestInterceptor6 -> OriginalResponse -> ResponseInterceptor6 -> ResponseInterceptor5 -> ResponseInterceptor4 -> ResponseInterceptor3 -> ResponseInterceptor2 -> ResponseInterceptor1
*边注[1]

这里为何要进行反转呢?简单来讲,是为了让调用者更加符合使用习惯。

这其实是装饰器模式的一部分(装饰栈顺序)。在 Go 语言中,装饰器以高阶函数的形式实现,当我们在函数中返回一个新的函数时,就添加了一层新的"装饰"。因此,当你在 WithMiddleware 函数中连续调用多个装饰器时,最后调用的装饰器首先被执行,这就产生了一个装饰顺序相反的情况。

为了形象地理解这个过程,你可以将装饰器想象成一堆礼物盒(套娃),你在最外层的盒子里放了一个小玩具(这是最初的处理函数),然后你将这个盒子放入一个稍大一些的盒子中,再将这个大一点的盒子放入更大的盒子中......,这样你就得到了一个嵌套的礼物盒。

现在假设你想打开这个礼物,你需要从最外层的盒子开始打开,一层一层往下,最后才能拿到里面的玩具。同样地,当你有多个装饰器时,你将首先执行最后一个装饰器(也就是你最后调用的那个装饰器,即最外层的盒子),然后依次向内执行,直到你到达最初的处理函数(即最里面的小玩具)。

*边注[2]

如果你希望一个特定的装饰器(如恢复 Panic recovery 的装饰器)始终首先执行,你应该将它放在所有其他装饰器之前。在这种情况下,由于顺序相反的特性,你需要手动反转装饰器数组,确保代码逻辑按照你想要的顺序去执行。

综合评估

类别 内容
优势
  1. 强大的扩展性:装饰器模式允许我们在不修改原始函数定义的情况下,向函数添加新的行为或责任。这极大地提高了代码的可扩展性。
  2. 优秀的灵活性:装饰器模式对于处理请求管道,例如HTTP请求的处理过程,非常有效。每一个中间件都可以被认为是一个装饰器,可以很方便的添加或移除。
  3. 保持代码整洁:装饰器模式能够保持业务逻辑的整洁和清晰,因为所有的装饰工作都是在原始函数之外完成的。
劣势
  1. 代码可读性:如果装饰器链太长或者装饰逻辑太复杂,那么阅读和理解代码可能会变得困难。
  2. 执行流程理解:由于装饰器的执行顺序和函数调用顺序通常是相反的,初学者可能需要一些时间来理解这个过程。
  3. 执行效率:添加过多的装饰器可能会导致额外的时间开销,可能需要额外的优化。

装饰器模式是一种非常强大和灵活的设计模式,对于一些特定的场景如 HTTP 请求的处理非常有用。它使得我们可以在不动态修改已有函数功能的基础上,为函数添加额外的行为或责任。

然而,使用装饰器模式时,我们需要注意以下几点:“装饰栈的顺序”、“装饰器的逻辑历程” 等。

另外,装饰器模式的适用场景主要是如下:

  1. 对函数功能的动态扩展:使用装饰器模式,我们能为函数添加新的特性或行为,而无需更改原来的函数定义。这在许多场景都十分有用。
  2. 处理HTTP请求:对于许多 Web 应用场景,装饰器模式可以应用于 HTTP 请求处理管道,实现诸如日志记录、错误处理、认证等功能。比如在标准库 net/http 或是第三方库 Gin 等中,装饰器模式都得到了广泛的应用。
  3. 分离关注点:如果某个函数负责的职责过多,可以通过装饰器模式将其进行分解,每个装饰器负责处理特定的逻辑,使得代码更加方便维护和阅读。

总的来说,虽然装饰器模式可能在理解和使用上存在一定的挑战,但对于许多编程场景来说,其优点是无法忽视的,并且基于这种模式,我们可以构建出具有良好可扩展性和可维护性的应用。

外观模式

外观模式(Facade Pattern)是一种结构性设计模式,它提供一个统一的接口,用于访问子系统中的一群接口,简化复杂系统的使用,也常称为 “门面” 模式。

image.png

代码示例

下面这个示例是一个使用了 "外观模式" 的音频播放器系统。它通过抽象出一个统一的接口,让用户端无需知道播放音频文件的全部复杂步骤。

在这个系统中,我们有三个主要的子系统:

  1. FileLoader:负责从磁盘中加载待播放的音频文件。
  2. Decoder:负责解码已加载的音频原始字节数据。
  3. Player:负责播放解码后的音频数据。

所有这些步骤都被AudioPlayer这个 "外观" 封装起来,用户只需要和这个AudioPlayer交互,告诉它们想播放哪个文件。接下来就由AudioPlayer负责调度内部各个子系统工作,整个调度过程就是 FileLoader -> Decoder -> Player。也就是,先由FileLoader加载音频文件,然后Decoder对加载的数据进行解码,最后Player播放解码后的音频。

codebase
├── audio
│   ├── decoder
│   │   └── decoder.go
│   ├── file
│   │   └── file.go
│   ├── index.go
│   └── player
│       └── player.go
│
└── main.go
// audio/decoder/decoder.go
package decoder

import (
    "fmt"
)

// 解码器模块
type Decoder struct{}

func (d *Decoder) Decode(data []byte) string {
    return fmt.Sprintf("%s", data)
}
// audio/file/file.go
package file

import (
    "os"
)

// 文件加载模块
type FileLoader struct{}

func (f *FileLoader) Load(filePath string) []byte {
    data, err := os.ReadFile(filePath)
    if err != nil {
        panic(err)
    }
    return data
}
// audio/player/player.go
package player

import (
    "fmt"
)

// 播放器模块
type Player struct{}

func (p *Player) Play(data string) {
    fmt.Println("播放:" + data)
}
// audio/index.go
package audio

import (
    "codebase/audio/decoder"
    "codebase/audio/file"
    "codebase/audio/player"
)

// 该类就是外观类
type AudioPlayer struct {
    loader  file.FileLoader
    decoder decoder.Decoder
    player  player.Player
}

func (a *AudioPlayer) Play(file string) {
    data := a.loader.Load(file)
    raw := a.decoder.Decode(data)
    a.player.Play(raw)
}
// main.go
package main

import (
    "codebase/audio"
)

func main() {
    player := audio.AudioPlayer{}
    player.Play("my_favorite_music.mp3")
}

综合评估

类别 内容
优势
  1. 简化接口:外观模式为一组子系统提供了一个简化的接口,使得子系统更易于使用。
  2. 解耦合:外观模式帮助客户端和子系统之间解耦,使得子系统的变化不会直接影响到客户端。
  3. 有助于分层:外观模式能够定义系统的入口,有助于将系统分层,简化系统之间的依赖关系。
  4. 代码整洁,易于维护:接口简化后,客户端的代码更清晰,也便于后期的维护和代码管理。
劣势
  1. 不符合开闭原则:如果需要增加新的子系统,可能需要修改外观类,这违反了开闭原则。
  2. 可能增加复杂性:在某些情况下,如果新增很多外观类,反而可能增加系统的复杂性。

在许多基于 Gin 框架(或其他 WF)的工程化项目中,你会发现有一种经常被使用的组织项目的方式:根目录下存在一个名为 controller 的目录,也就是我们所谓存放路由视图函数的地方。这个目录下有许多子目录,如 authuserpostcomment 等,而这些目录基本上代表了不同的分支 uri。在每个子目录下又存在许多子路由 endpoints

我们通常需要逐级在每个子目录下创建名为 index.go(或 entry.go)的文件,以收敛该目录下的所有路由。并使用unit-like structempty 结构体,可以视为 headless 结构体)进行绑定。这样的设计使我们可以在 router 中便捷地进行函数与路径的映射。

虽然这里只是用文字描述,没有具体的代码示例,这可能使那些对此不熟悉的同学感到有些困惑。然而,这种做法的优点是明显的,那就是我们不再需要在 router 中导入一大堆的端点,只需要简单地引用一个 controller 的门面。

因此,可以说,这种便于导航和管理复杂系统的门面模式,在许多开发实践中都有所体现。这种模式几乎无处不在,为我们提供了一种方便高效的工具。

享元模式

享元模式(Flyweight Pattern)是一种结构性设计模式,通过共享对象来有效地支持大量细粒度的对象,以减少内存使用和提高性能,也常称为 "缓存", "cache" 模式。

image.png

代码示例

下面这个示例中,我们来模拟围棋游戏中的棋子放置过程。由于每个围棋棋子的颜色只有黑白两种,所以我们可以用享元模式来减少内存中棋子对象的数量。“享元” 这个词其实就是 "共享元素" 的意思。

在下棋这类游戏中,棋子的形状和颜色是固定,不会发生变化。因此,我们提取出这些固定的属性,也就是棋子的"内部状态"。然后,棋子的放置位置,由于游戏需要有复盘、回退、悔棋的功能,必需要有记忆功能,这个则是"外部状态",需要单独记录。

在进行棋子下棋的动作时,我们并不需要复制棋子的所有信息,只需记录每一步棋对应的棋子以及它的位置即可。通过指向相应颜色的棋子实例,我们能追踪到每一个棋子,同时了解它被放置在棋盘的何处。

如此一来,我们避免了每一步棋都创建新棋子的冗余操作,极大地节省了内存使用。这正是享元模式的优势所在。

// gof/flyweight.go
package gof

import (
    "fmt"
)

// GoPiece 是代表围棋棋子的类。颜色是棋子的内部状态
type GoPiece struct {
    Color string
}

// Put 方法用于在棋盘上放置棋子,位置(x,y)是棋子的外部状态
func (g *GoPiece) Put(x, y int) {
    fmt.Printf("将 %s 色的棋子放在 %d, %d 的位置\n", g.Color, x, y)
}

// GoPieceFactory 是代表围棋棋子工厂的类。它用于创建并管理围棋棋子
type GoPieceFactory struct {
    Pieces map[string]*GoPiece
}

// GetGoPiece 方法用于获取一个特定颜色的围棋棋子。如果这个颜色的棋子已经存在,那么就返回已经存在的棋子,否则就创建一个新的棋子
func (f *GoPieceFactory) GetGoPiece(color string) *GoPiece {
    if _, exist := f.Pieces[color]; !exist {
        f.Pieces[color] = &GoPiece{Color: color}
    }
    return f.Pieces[color]
}
// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
    // 创建一个新的围棋棋子工厂
    factory := &gof.GoPieceFactory{Pieces: make(map[string]*gof.GoPiece)}

    // 1. 创建黑色棋子并放置
    black := factory.GetGoPiece("黑")
    black.Put(1, 2)

    // 2. 创建白色棋子并放置
    white := factory.GetGoPiece("白")
    white.Put(2, 3)

    // 3. 再次获取黑色棋子并放置
    black2 := factory.GetGoPiece("黑")
    black2.Put(3, 4)

    // 打印创建的棋子总数
    fmt.Println("棋子总数:", len(factory.Pieces)) // 输出:棋子总数:2
}

综合评估

类别 内容
优点
  1. 通过复用已经存在的对象,享元模式可以大量减少内存的使用,特别适用于大量的对象实际上是相同或相似,但需要频繁创建和销毁的场景。
  2. 由于复用对象,可以减少系统中对象的数量,这使得对象的管理和维护变得更容易。
  3. 享元模式将对象的状态分为内部状态和外部状态,使得对象的行为可以在运行时动态改变,提高了系统的灵活性。
缺点
  1. 享元模式将对象信息的取得过程复杂化,需要分辨出哪些信息可以内化,哪些信息可以外化。
  2. 这种模式需要维护一个存储享元对象的享元池,而这可能会因额外的复杂性、存储开销和获取开销而抵消节省的内存。
  3. 由于享元模式需要提供大量细粒度的类/对象,可能会引入过多的对象,导致系统变得复杂。

享元模式需要在节省内存和代码复杂度之间做平衡,如果系统中不包含大量相同或相似的对象,或者这些对象的创建和销毁对资源消耗没有明显影响,那么就没有必要使用享元模式。

只有在程序需要支持大量对象,但具备的内存空间受限的时候,使用享元模式才是明智的。它可以在内存资源紧张的情况下,灵活地支持大量的对象。也就是说,享元模式带来的益处高度依赖于如何并在何种情况下使用。

享元模式在游戏开发中的应用非常广泛。在许多应用场景中,程序对对象的身份并无严格依赖。例如,在棋盘游戏中,关注的是棋子的颜色和位置,而非棋子的单个身份。类似的,在 Dota 中的森林元素,CSGO 中的每一位战士,或者 "太空大战" 里的每一颗子弹和它们的飞行路径。在这些情景中,尽管对象在屏幕上的表现和互动可能各不相同,但在程序设计层面上,我们只关注它们的状态和行为,而非对它们单一的身份进行跟踪。这种灵活的编程思维,是享元设计模式的实际应用场景之一。

经典热知识

《超级马里奥》是由任天堂公司开发的一款电子游戏,它于 1985 年首次发售。当时的电子游戏硬件资源非常有限,一款典型的家用游戏机,像任天堂的 NES “红白机” 只有 32KBROM 空间用于存放游戏程序和数据。简单来说,这意味着开发者只有约 32000 个字节的空间来创建一个完整的游戏——这是一项巨大的挑战。

即使面临这样的硬件限制,开发者们还是成功地塑造了一个生动丰富的游戏世界。为了实现这样的结果,他们在游戏开发中大量使用了各种优化和压缩技术,而享元模式就是其中的一种被用于优化游戏性能的方式。

以下是在《超级马里奥》中一些复用内存的典型场景:

  1. 游戏环境元素:游戏中有很多的环境元素,比如砖块、云朵、山脉等,都可以使用享元模式来创建。由于这些元素的形状、颜色和大小都是固定的,所以它们是内部状态;而它们在游戏地图中的位置则是外部状态。这样一来,我们就可以为每种环境元素只创建一个对象,然后通过改变元素的位置来在地图上生成多个“实例”。
  2. 敌人角色:像乌龟、蘑菇、飞鱼这些敌人角色,它们的形状和颜色都是固定的,且游戏中可能会有大量这些敌人存在。同样地,我们可以使用享元模式来创建这些敌人角色,这样就可以大幅度减少游戏中对象的数量,提高性能。
  3. 道具和特效:游戏中的金币、道具以及各种特效也可以使用享元模式来创建。比如我们可以为爆炸特效创建一个对象,然后在游戏中的不同地方复用这个对象,只需要改变特效发生的位置和时间即可。

20240102-022752.gif

代理模式

代理模式(Proxy Pattern)是一种结构性设计模式,它为其他对象提供一个代理,以控制对这个对象的访问,并提供额外的功能,例如延迟加载、权限控制等。

image.png

代码示例

Kubernetes 中,LoadBalancer 通常用于在集群外部暴露服务。LoadBalancerService 的一种类型,通常由云提供商提供,但对于不在云环境中运行的集群,可能需要使用其他解决方案来实现这个功能。

这个时候就可以使用代理模式来实现。一个外部的代理服务器可以作为集群和外部流量之间的中间人,它接收外部的流量,然后按照预先定义的规则将流量路由到集群内部的 Service。外部的客户不需要直接和集群内部的 Service 交互,而是通过代理服务器,这增强了集群的安全性。

基于这个设计,我们可以在代理服务器上实现各种功能,比如负载均衡、流量控制、安全策略等,而不需要修改集群内部的 Service

以下是一个简易版的代理模式用于 K8s LoadBalancer 的示例:

// Cluster 是集群, 是真实服务所在, 其中包含多个 Service
type Cluster struct {
    Services []Service
}

// Service 是服务, 具体的业务处理逻辑在这里
type Service struct {
    Name string
}

// 把请求发送给服务
func (s *Service) Serve(request Request) {
    // 这里是服务处理逻辑
}

// LoadBalancer 是代理, 代理在集群和外部流量之间进行中转
type LoadBalancer struct {
    cluster *Cluster  // 这里引用了一个集群, 真实的服务在这里.
}

// 外部的请求由 LoadBalancer 的 Serve 方法进行接收,
// 方法根据服务名在集群中查找对应的 Service, 然后把请求转发给这个 Service
func (lb *LoadBalancer) Serve(serviceName string, request Request) {
    for _, service := range lb.cluster.Services {
        if service.Name == serviceName {
            // 当找到了服务名对应的服务后, 把请求转发给这个服务
            service.Serve(request)
        }
    }
}

综合评估

类别 内容
优势
  1. 控制访问: 代理可以控制对其代表的对象的访问,可以执行一些操作(例如检查权限,记录日志等),然后转发请求。
  2. 延迟初始化: 如果一个对象的创建耗时长且消耗资源,代理可以延迟其创建,直到真正需要它时再创建。
  3. 能够对被代理对象进行额外的功能扩展。
  4. 可以实现网络代理,使本地代码能够访问远程对象。
劣势
  1. 代码复杂度增加: 需要实现代理类。
  2. 可能导致请求处理的延迟。
  3. 某些情况下可能被误用,如过度使用代理。
  4. 引入了一个新的抽象层,可能影响系统性能。

行为型模式

责任链模式

责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它将请求的发送者和接收者解耦,并将多个接收者连成一条链,以处理该请求,直到有一个接收者能够处理它为止。

image.png

代码示例

下面这个代码示例实现的是一个使用责任链模式的请假审批流程。在这个例子中,我们有三个主要的参与者:TeamLeaderDepLeaderCEO,它们都实现了 Manager 接口。每个 Manager 都可以处理请假申请,并决定是否将申请传递给下一个 Manager

具体来说:

  1. TeamLeader: 具有部分请假审批权,处理时长在 < 3d 的请假申请。> 3d 的申请,会转发至下一级的审批者。
  2. DepLeader: 处理时长在 3 < d < 7 以内的请假申请。> 7d 的申请,会转发至下一级的审批者。
  3. CEO: 处理时长在 7 < d < 30 以内的请假申请。超过 30d 的申请将被拒绝。

每个 Manager 有下一个 Manager 的引用,这样一来,当一个 Manager 无法处理某个请求时,它就可以将请求转发给下一个 Manager,实现请求的连续处理,这就是责任链模式的精髓。这也使得 Manager 们之间的关系可以在运行时动态改变,为系统提供了良好的灵活性。

// gof/chain_of_responsibility.go
package gof

import (
    "fmt"
)

// Manager 接口可以设置和处理各级别的请假请求
type Manager interface {
    SetNext(Manager)              // SetNext 方法设置下一级别的管理者
    Handle(name string, days int) // Handle 方法用于处理请假请求,参数有请假者的名称和请假时长
}

type TeamLeader struct {
    next Manager
}

func (t *TeamLeader) SetNext(m Manager) {
    t.next = m
}

func (t *TeamLeader) Handle(name string, days int) {
    if days <= 3 {
        fmt.Printf("Team Leader approves %s's %d day(s) off.\n", name, days)
    } else {
        if t.next != nil {
            t.next.Handle(name, days)
        }
    }
}

type DepLeader struct {
    next Manager
}

func (d *DepLeader) SetNext(m Manager) {
    d.next = m
}

func (d *DepLeader) Handle(name string, days int) {
    if days <= 7 {
        fmt.Printf("DepLeader approves %s's %d day(s) off.\n", name, days)
    } else {
        if d.next != nil {
            d.next.Handle(name, days)
        }
    }
}

type CEO struct {
    next Manager
}

func (c *CEO) SetNext(m Manager) {
    c.next = m
}

func (c *CEO) Handle(name string, days int) {
    if days <= 30 {
        fmt.Printf("CEO approves %s's %d day(s) off.\n", name, days)
    } else {
        fmt.Printf("%s's request for %d day(s) off is rejected.\n", name, days)
    }
}
// main.go
package main

import (
    "codebase/gof"
)

func main() {
    tl := &gof.TeamLeader{}
    dl := &gof.DepLeader{}
    ceo := &gof.CEO{}

    // 设置审批流程
    tl.SetNext(dl)
    dl.SetNext(ceo)

    // 发起请假请求
    tl.Handle("Bob", 2)
    tl.Handle("Alice", 5)
    tl.Handle("Tom", 10)
    tl.Handle("Jerry", 35)
}

综合评估

类别 内容
优势
  1. 简化了对象之间的连接,请求的发送者不必知道请求在何处以及谁被处理,接收者也不需要知道整个请求的全貌。每个对象只需要知道自己的下一个责任链对象即可。
  2. 可以在运行时动态改变接收者和责任链的顺序,可以灵活地添加或删除责任。
  3. 提供了对请求的处理,并通过改变链内的成员或调整链的顺序,允许动态地新增或者删除责任。
劣势
  1. 一个请求可能因为没有任何接收者处理它而宣告失败。由于没有明确的接收者,所以无法保证请求一定被处理,需要在设计时额外注意。
  2. 对于比较长的责任链,请求的处理可能会耗费较长时间,有可能导致性能的降低。
  3. 如果设计不当,责任链可能会导致循环调用,将引起系统的死锁。

责任链模式主要用于解耦请求的发送者和接收者,通过一系列接收者链来处理请求,直到一个对象处理该请求或该请求在链的末端得不到处理。这个模式有强大的灵活性,可以在运行时改变责任的层级和顺序。我们在使用责任链模式时,需要特别注意链的顺序、链的长度和链成员的角色定义,避免可能带来的负面影响。

命令模式

命令模式(Command Pattern)是一种行为型设计模式,它将请求封装成对象,以使得可以用不同的请求对客户端进行参数化,同时支持请求的排队、记录和撤销操作。

image.png

解释器模式

解释器模式(Interpreter Pattern)是一种行为型设计模式,它定义了一个语言的文法,并且建立一个解释器来解释该语言中的句子,以执行特定的操作。

迭代器模式

迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供一种方法来顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

image.png

代码示例

迭代器在 Go 中是一个十分常见的模式,它被用在许多内置的数据结构中,如切片、数组等。在 Go 中,我们经常使用 range 关键字来创建迭代器。

当然,我们也可以自定义迭代器,以下是一个简单的使用 Go 实现的迭代器示例:

// gof/iterator.go
package gof

// 定义聚合对象应实现的接口
type Aggregate interface {
    Iterator() Iterator
}

// 定义迭代器应实现的接口
type Iterator interface {
    HasNext() bool
    Next() interface{}
}

// Numbers 是将要被迭代的聚合对象结构
type Numbers struct {
    start, end, step int
}

// NewNumbers 用来创建Numbers对象,根据传入参数的数量,决定start、end、step的值
func NewNumbers(v ...int) *Numbers {
    if len(v) == 0 {
        return &Numbers{}
    } else if len(v) == 1 {
        return &Numbers{1, v[0], 1}
    } else if len(v) == 2 {
        return &Numbers{v[0], v[1], 1}
    } else {
        return &Numbers{v[0], v[1], v[2]}
    }
}

// 实现Aggregate接口的Iterator方法,返回一个NumbersIterator
func (n *Numbers) Iterator() Iterator {
    return &NumbersIterator{
        numbers: n,
        next:    n.start,
    }
}

// NumbersIterator 定义Numbers结构的迭代器信息
type NumbersIterator struct {
    numbers *Numbers
    next    int
}

// 通过比较next和end的值,来判断是否还有下一个元素
func (i *NumbersIterator) HasNext() bool {
    if i.next <= i.numbers.end {
        return true
    }
    return false
}

// 返回next当前的值,并将next的值增加step
func (i *NumbersIterator) Next() interface{} {
    if i.HasNext() {
        next := i.next
        i.next += i.numbers.step
        return next
    }
    return nil
}
// main.go
package main

import (
    "fmt"

    "codebase/gof"
)

func main() {
    //numbers := gof.NewNumbers(10)
    //numbers := gof.NewNumbers(1, 10)
    numbers := gof.NewNumbers(1, 10, 2)

    iterator := numbers.Iterator()

    for iterator.HasNext() {
        fmt.Println(iterator.Next()) // Output: 1 3 5 7 9
    }
}

综合评估

类别 内容
优势
  1. 支持以不同的方式遍历集合,可以使这些遍历相互独立。
  2. 实现了单一职责原则,你可以将数据遍历功能从集合中抽取出来,得到一个新的类。这会减轻主类的负担,并使得代码更清晰。
  3. 当你在类中实现多种遍历,则可以使用迭代器类,这样无需修改集合类代码。
劣势
  1. 使用了该模式可能会增加代码的复杂性。
  2. 如果访问模式经常更改,可能会导致频繁修改迭代器的接口和实现,维护成本提高。

由于 Go 语言中已经在内建的容器类型(如切片和映射)中提供了 range 关键字来支持迭代功能,因此在 Go 的实际项目开发中使用自定义迭代器模式的情况也就相对较少了。

中介者模式

中介者模式(Mediator Pattern)是一种行为型设计模式,它用一个中介对象来封装一系列对象之间的交互,使得这些对象之间不需要显式地相互引用,从而使其耦合松散。

image.png

代码示例

在这个 Go 示例代码中,我们实现了一个房地产交易场景的中介者模式。在房地产交易中,买家 (Buyer) 与卖家 (Seller) 往往不能直接进行交流,他们需要通过房地产中介 (EstateAgent) 进行沟通。

中介者模式通过 Mediator 接口来定义,买家和卖家则通过实现 Person 接口来实现各自的消息处理。这使得买家和卖家能够自由地添加或修改消息处理的逻辑,而无需修改买家和卖家自身的代码。同时,所有的消息传递都经过中介者,这使得消息流动的路径和规则集中管理,提高了代码的可维护性。

// gof/mediator.go
package gof

import (
    "fmt"
)

// Mediator 定义中介者接口,封装了发送消息的方法。
type Mediator interface {
    Send(message string, person Person)
}

// Person 定义人员接口,封装了接收消息的方法。
type Person interface {
    Notify(message string)
}

// Buyer,代表买家,实现了 Person 接口。
type Buyer struct {
    Mediator Mediator
}

// Seller,代表卖家,实现了 Person 接口。
type Seller struct {
    Mediator Mediator
}

// 买家接收消息后,将消息打印出来。
func (b *Buyer) Notify(message string) {
    fmt.Println("Buyer received message:", message)
}

// 卖家接收消息后,将消息打印出来。
func (s *Seller) Notify(message string) {
    fmt.Println("Seller received message:", message)
}

// EstateAgent,代表房地产中介,实现了 Mediator 接口。
type EstateAgent struct {
    Buyer  *Buyer
    Seller *Seller
}

// 根据消息的发送者,确定消息的接收者,并将消息发送给接收者。
func (ea *EstateAgent) Send(message string, person Person) {
    if person == ea.Buyer {
        ea.Seller.Notify(message)
    } else {
        ea.Buyer.Notify(message)
    }
}
// main.go
package main

import (
    "codebase/gof"
)

func main() {
    // 创建一个 EstateAgent 实例
    agent := &gof.EstateAgent{}

    // 创建一个 Buyer 实例,并将其 Mediator 设置为上面创建的 EstateAgent
    buyer := &gof.Buyer{
        Mediator: agent,
    }
    // 创建一个 Seller 实例,并将其 Mediator 设置为上面创建的 EstateAgent
    seller := &gof.Seller{
        Mediator: agent,
    }

    // 将 EstateAgent 中的 Buyer 和 Seller 设置为上面创建的 Buyer 和 Seller
    agent.Buyer = buyer
    agent.Seller = seller

    // Buyer 通过 Mediator(EstateAgent) 发送一个消息
    buyer.Mediator.Send("Is the house still available?", buyer)

    // Seller 通过 Mediator(EstateAgent) 发送一个消息
    seller.Mediator.Send("Yes, please offer.", seller)
}

综合评估

类别 内容
优势
  1. 降低耦合度:中介者模式降低了对象之间的耦合,使得对象易于独立地被复用。
  2. 集中控制交互:多个对象的交互被集中控制在一个中介者对象中,系统更易于理解和维护。
劣势
  1. 中介者对象的复杂性:由于全部的交互控制逻辑都集中在中介者对象中,使得中介者对象变得过于复杂,这是该模式最大的缺点。
  2. 系统的不透明性:通过引入中介者对象,系统的交互关系被隐蔽在中介者中,这增加了系统的不透明性和复杂性。

中介者模式解决的主要问题是复杂对象之间的通信,它定义了一个中介对象来封装这些对象之间的交互。虽然它降低了系统的耦合性,提高了系统的灵活性,但同时也带来了中介者的复杂性。因此,在设计时,要根据实际需要考虑是否需要使用中介者模式,并在确保系统的可维护性的同时,尽可能地减少系统的复杂性。

备忘录模式

备忘录模式(Memento Pattern)是一种行为型设计模式,它在不违反封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后可以将该对象恢复到原先保存的状态。

image.png

观察者模式

观察者模式(Observer Pattern)是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。

image.png

状态模式

状态模式(State Pattern)是一种行为型设计模式,它允许对象在内部状态改变时改变它的行为,对象看起来好像改变了它的类。

image.png

策略模式

策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,并将它们封装成独立的策略类,使得它们可以互相替换,使得算法的变化独立于使用算法的客户端。

image.png

模板方法模式

模板方法模式(Template Method Pattern)是一种行为型设计模式,它定义了一个操作中的算法骨架,将某些步骤的具体实现延迟到子类中,以使子类可以不改变算法结构的情况下重新定义该算法的某些特定步骤。比如在 Java、C# 中,抽象类常常被应用在模板设计模式中。

image.png

访问者模式

访问者模式(Visitor Pattern)是一种行为型设计模式,它定义了对某对象结构中各元素的操作方式,可以在不改变这个结构的情况下定义新的操作。

image.png

非 GoF 模式

GoF 列出了 23 种经典的设计模式,但实际上在编程实践中,有其他一些模式也被社区和程序员们广泛接受和使用,主要包括:

设计模式描述
简单工厂模式(Simple Factory)并未出现在 GoF 的 23 个设计模式中,但是它的思想非常简单,主要用于创建某类对象,封装了创建对象的逻辑。
空对象模式(Null Object)空对象替换 NULL 对象实例的检查。一个空对象不是检查空值,而是反应一个不做任何动作的关系。这样的空对象也可以在数据不可用的时候提供默认的行为。
线程池模式(Thread Pool)这是一种在设计多线程应用程序时常用的模式。一个线程池是一组闲置的线程,这些线程处于等待状态,预备在程序中的其他线程分配给它们的任务去执行。
管道模式(Pipeline)管道模式旨在管道中的一系列操作中处理数据。该模式允许我们按照需要动态修改或组合处理过程。
依赖注入模式(Dependency Injection)它是一种实现控制反转 IoC (Inversion of Control) 的技术,使得代码更加解耦合。在这种模式中,一个类获得它依赖的对象的引用,而不是自己去构造或找到这些对象。
MVC 模式(Model View Controller)是一种架构模式,广泛应用于 Web 开发中。
MVVM 模式(Model View ViewModel)框架模式,用于解决UI开发中的分离问题,如 WPF 和 Angular。

简单工厂模式

代码示例

简单工厂模式(Simple Factory Pattern)是后来由社区提出的一种创建型设计模式,它通过一个集中的工厂类根据给定的输入或条件来创建对象,也被称为 "静态工厂模式"(static factory method)。

示例一

"简单工厂模式" 主要用于创建某一种或某一类对象。在简单工厂模式里,你可以想象,所有的产品都是从同一间工厂流水线上生产出来的,不同的产品只是工厂生产流程的不同分支。

从某些角度来看,简单工厂模式和工厂方法模式是有点相似,因为它们都提供了一种方式来封装对象的创建逻辑。然而,这两种模式的用途和结构存在明显的差异。

我们还是使用餐馆举例,便于理解:

package main

import "fmt"

// Food 是我们的基本产品接口
type Food interface {
    Cook() string
}

// Burger 是一种具体的食物
type Burger struct {}
func (b Burger) Cook() string {
    return "Making a burger."
}

// Dumpling 也是一种具体的食物
type Dumpling struct {}
func (d Dumpling) Cook() string {
    return "Making dumplings."
}

// FoodFactory 是我们的简单工厂,用来创建食物
func FoodFactory(foodType string) Food {
    switch foodType {
    case "Burger":
        return &Burger{}
    case "Dumpling":
        return &Dumpling{}
    default:
        return nil
    }
}

func main() {
    food1 := FoodFactory("Burger")
    fmt.Println(food1.Cook())
    
    food2 := FoodFactory("Dumpling")
    fmt.Println(food2.Cook())
}

示例二

Go 语言中,常会用函数创建并初始化结构体的方式来实现简单工厂模式,这类函数通常会有 "New" 开头的函数名。其实这和简单工厂模式的原理是一样的,Simple Factory 不一定是一个类,很多情况下,它只是一段代码,或者一个函数。

这种 NewXXX 的方式在 Go 语言编程中非常常见。原因有二:其一,Go 语言没有构造函数;其二,结构体的零值不一定是我们想要的初始状态。

package main

import (
    "fmt"
)

type User struct {
    name string
    age  int
}

func NewUser(name string, age int) *User {
    return &User{name: name, age: age}
}

func main() {
    user := NewUser("John", 20)
    fmt.Println(user) 
}

管道模式

管道模式(Pipe and Filter)数据经过一系列处理器进行处理,每个处理器都会对数据进行处理然后将结果传递给下一个。处理器一般会按照顺序执行,每个处理器都处理自己的部分,然后将结果传递给下一个。处理器之间没有控制权,只负责处理数据。

管道模式是责任链模式的一个变体,它更关注的是如何处理数据。这种模式在流程控制、数据处理中非常常见,被广泛应用于函数式编程、流式编程中。

代码示例

下面这种方式,我们可以构建有序的函数调用链并执行,类似于中间件的处理方式。当然,下面的示例仅作为演示。在实际业务中,如果你想要校验各个字段的有效性,我们可以搭配使用 validator 库来实现。

// main.go
package main

import (
    "errors"
    "fmt"
    "regexp"
)

// 定义处理函数类型
type HandlerFunc func(user *User) error

// User 数据结构
type User struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
}

func main() {
    // 创建 User 实例
    user := &User{"test", "test@example.com", 30}

    // 将User实例和处理函数作为参数,传入SomeService函数
    SomeService(user, handleUsername, handleEmail, handleAge)

    // 其它业务逻辑
    // ...
}

func SomeService(user *User, handlers ...HandlerFunc) {
    // 迭代调用所有处理函数
    for _, handler := range handlers {
        err := handler(user)
        if err != nil {
            fmt.Printf("Error occurred: %s\n", err.Error())
            return
        }
    }

    fmt.Println("All handlers finished successfully")
}

func handleUsername(user *User) error {
    // 如果用户名为空或者长度大于10,返回错误
    if len(user.Username) == 0 || len(user.Username) > 10 {
        return errors.New("Username error: length should be between 1 and 10")
    }
    return nil
}

func handleEmail(user *User) error {
    // 检验邮箱格式是否正确
    if match, _ := regexp.MatchString(`^[\w-]+(.[\w-]+)*@[\w-]+(.[\w-]+)+$`, user.Email); !match {
        return errors.New("Email error: not a valid email address")
    }
    return nil
}

func handleAge(user *User) error {
    // 检验年龄是否在1-99之间
    if user.Age > 99 || user.Age < 1 {
        return errors.New("Age error: value should be between 1 and 99")
    }
    return nil
}

函数选项模式

函数选项模式(Functional Options),在 Go 社区中普遍得到了独特的喜爱。这个模式为构造函数的调用者提供了极大的灵活性,使得用户可以在保留默认行为的基础上进行任意自定义的配置。其本质上是函数式编程的一种应用方式。

为何 Go + 函数选项才算是绝配?

函数选项模式在 Go 社区的广泛流行,主要归功于其优雅地解决了在 Go 编程中常见的一些问题。

image.png

首先,Go 是一门静态类型的语言,且不具备类似 Java 中的构造器重载或 C# 中的命名参数等特性。因此,当一个函数(比如结构体的构造函数)具有许多可选参数时,我们通常需要提供一个包含多字段的配置结构体作为输入。然后,这个配置结构体需要在创建时初始化其所有字段,然而这就带来了一些额外的工作,且使得调用者在理解每个字段具体作用上有所困难。而函数选项模式则提供了一个清晰且简洁的方式来处理这个问题。

其次,Go 的函数是一等公民,并且支持闭包,这使我们可以自然而然地实现函数选项模式。与需要编写大量样板代码的 Builder 模式相比,函数选项模式更加简洁,灵活且易于理解。

最后,函数选项模式由于其灵活性质,也有助于维护 API 的向后兼容性。当你需要增加新的选项时,只需要添加一个新的函数选项即可,无需改变现有的函数签名或是调用代码。

当然,元年后诞生的编程语言基本都支持多编程范式。譬如 Rust,其实也可将函数视为一等公民,并且支持闭包,但由于 Rust 的所有权和生命周期规则,使得在 Rust 中实现函数选项模式可能会相比 Go 中更复杂一些。

至于动态语言,如 PythonJavaScriptPython 有独特的 *args/**kwargs 机制,JS 同样也有 Rest parameters 和解构等方式。这些情况都显示出了,不同的语言,都有着属于自己独特的编程实现方式。

确实可以写,但也属实没必要系列

Python 初始化

# Python 版函数选项
class DatabaseConfig:
    def __init__(self, *options):
        self.options = options
        self.host = 'localhost'
        self.port = 3306
        self.user = 'root'
        self.password = 'root'
        self.database = 'test_db'

        for option in options:
            option(self)

def set_host(host):
    def inner(config):
        config.host = host
    return inner

def set_port(port):
    def inner(config):
        config.port = port
    return inner

def set_user(user):
    def inner(config):
        config.user = user
    return inner

def set_password(password):
    def inner(config):
        config.password = password
    return inner

def set_database(database):
    def inner(config):
        config.database = database
    return inner

config = DatabaseConfig(set_host('192.168.1.1'), set_port(8888), set_user('admin'), set_password('admin123'), set_database('my_database'))
print(config.host)      # 输出 192.168.1.1
print(config.port)      # 输出 8888
print(config.user)      # 输出 admin
print(config.password)  # 输出 admin123
print(config.database)  # 输出 my_database
# 经典的 Python 风格
class DatabaseConfig:
    def __init__(self, host='localhost', port=3306, user='root', password=None, database=None):
        self.host = host
        self.port = port
        self.user = user
        self.password = password
        self.database = database

config = DatabaseConfig(host='192.168.1.1', port=8888, user='admin', password='admin123', database='my_database')
print(config.host)      # 输出 192.168.1.1
print(config.port)      # 输出 8888
print(config.user)      # 输出 admin
print(config.password)  # 输出 admin123
print(config.database)  # 输出 my_database

JavaScript 初始化

// Nodejs 版函数选项
class DatabaseConfig {
    constructor(...options) {
        this.options = options;
        this.host = 'localhost';
        this.port = 3306;
        this.user = 'root';
        this.password = 'root';
        this.database = 'test_db';

        options.forEach(option => option(this));
    }
}

const setHost = (host) => (config) => { config.host = host; };
const setPort = (port) => (config) => { config.port = port; };
const setUser = (user) => (config) => { config.user = user; };
const setPassword = (password) => (config) => { config.password = password; };
const setDatabase = (database) => (config) => { config.database = database; };

const config = new DatabaseConfig(
    setHost('192.168.1.1'), 
    setPort(8888), 
    setUser('admin'), 
    setPassword('admin123'), 
    setDatabase('my_database')
);
console.log(config.host);      // Outputs: 192.168.1.1
console.log(config.port);      // Outputs: 8888
console.log(config.user);      // Outputs: admin
console.log(config.password);  // Outputs: admin123
console.log(config.database);  // Outputs: my_database
// 经典的 ES6 风格
class DatabaseConfig {
    constructor({ host = 'localhost', port = 3306, user = 'root', password = 'root', database = 'test_db' } = {}) {
        this.host = host;
        this.port = port;
        this.user = user;
        this.password = password;
        this.database = database;
    }
}

const config = new DatabaseConfig({
    host: '192.168.1.1', 
    port: 8888, 
    user: 'admin', 
    password: 'admin123', 
    database: 'my_database'
});

console.log(config.host);      // Outputs: 192.168.1.1
console.log(config.port);      // Outputs: 8888
console.log(config.user);      // Outputs: admin
console.log(config.password);  // Outputs: admin123
console.log(config.database);  // Outputs: my_database

最后,来看下最正统的 Go 版本吧

作为一名 Gopher,对于函数选项模式我们有着较深的感情。因此,下面对这个模式进行特别介绍!

下面这段代码展示了如何使用函数选项模式来配置一个数据库连接。创建数据库连接需要一些参数,这些参数可能有默认值,并且在不同的场景下可能需要改变。函数选项模式提供了一种灵活,可读性高的方式来处理这种问题。每一个可选的参数都有一个对应的函数(例如 WithHost, WithPort 等),这些函数返回一个 Option,这个 Option 是一个有特殊定义的函数,它接收一个数据库配置对象,并更改这个对象的特定字段。

Chain 类型是 Option 类型的切片。这个类型通常被用来存储多个选项函数。Chain 上定义的 apply 方法会将每一项选项函数都应用到传入的数据库配置对象上,同时也起到隐蔽实现细节的作用。

然后在 NewDatabaseConfig 函数中,你先初始化一个默认的数据库配置对象,然后传入一系列的 Option 类型的函数,并使用 Chain(opts).apply(cfg) 来将这些函数应用到默认配置对象上,从而得到一个根据这些选项函数得到的最终配置对象。

// Golang 函数选项模式
package main

import (
    "fmt"
)

// DatabaseConfig 结构体定义了数据库配置信息
type DatabaseConfig struct {
    host     string
    port     int
    user     string
    password string
    database string
}

// Option 函数类型定义了修改配置的选项函数
type Option func(*DatabaseConfig)

// Chain 类型是 Option 函数的切片,用于存储多个选项函数
type Chain []Option

// WithHost 返回一个设置 host 的选项函数
func WithHost(host string) Option {
    return func(c *DatabaseConfig) {
        c.host = host
    }
}

// WithPort 返回一个设置 port 的选项函数
func WithPort(port int) Option {
    return func(c *DatabaseConfig) {
        c.port = port
    }
}

// WithUser 返回一个设置 user 的选项函数
func WithUser(user string) Option {
    return func(c *DatabaseConfig) {
        c.user = user
    }
}

// WithPassword 返回一个设置 password 的选项函数
func WithPassword(password string) Option {
    return func(c *DatabaseConfig) {
        c.password = password
    }
}

// WithDatabase 返回一个设置 database 的选项函数
func WithDatabase(database string) Option {
    return func(c *DatabaseConfig) {
        c.database = database
    }
}

// apply 方法将 Chain 中的选项函数应用到给定的配置对象上
func (chain Chain) apply(config *DatabaseConfig) {
    for _, option := range chain {
        option(config)
    }
}

// NewDatabaseConfig 创建一个新的数据库配置对象
func NewDatabaseConfig(opts ...Option) *DatabaseConfig {
    // 初始化默认配置
    cfg := &DatabaseConfig{
        host: "localhost",
        port: 3306,
    }
    
    // 将选项函数应用到配置对象上
    Chain(opts).apply(cfg)

    return cfg
}

func main() {
    // 使用选项函数创建数据库配置对象
    config := NewDatabaseConfig(
        WithHost("192.168.1.1"),
        WithPort(8888),
        WithUser("admin"),
        WithPassword("admin123"),
        WithDatabase("my_database"),
    )

    // 打印配置信息
    fmt.Printf("Host: %s\nPort: %d\nUser: %s\nPassword: %s\nDatabase: %s\n", config.host, config.port, config.user, config.password, config.database)
}

尽管这是一个以数据库连接为背景的例子,但并不意味着在业务中每次写数据库连接都得用到函数选项模式,过度使用可能反而会让事情变得无比复杂。我个人觉得,是完全没有那个必要。最重要的是看业务需求是怎样,大部分基础的 CRUD 业务逻辑一般不会用到这个模式,除非你真的有大量需要解耦的可选参数。实际上,这种模式更多的是在建设基础设施或工具库这类场景下使用。