• Go 学习笔记(90)— 常用设计模式(单例模式、工厂模式、策略模式、模板模式、代理模式、选项模式)


    从总体上说,设计模式可以分为创建型模式、结构型模式、行为型模式 3 大类,用来完成不同的场景。其中分类如下:
    设计模式

    1. 创建型模式

    它提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这种类型的设计模式里,单例模式和工厂模式(具体包括简单工厂模式、抽象工厂模式和工厂方法模式三种)在 Go 项目开发中比较常用。

    1.1 单例模式

    单例模式,是最简单的一个模式。在 Go 中,单例模式指的是全局只有一个实例,并且它负责创建自己的对象。单例模式不仅有利于减少内存开支,还有减少系统性能开销、防止多个实例产生冲突等优点。

    因为单例模式保证了实例的全局唯一性,而且只被初始化一次,所以比较适合全局共享一个实例,且只需要被初始化一次的场景,例如数据库实例、全局配置、全局任务池等。

    单例模式又分为饿汉方式和懒汉方式。

    • 饿汉方式指全局的单例实例在包被加载时创建;
    • 懒汉方式指全局的单例实例在第一次被使用时创建;

    1.1.1 饿汉方式

    package singleton
    
    type singleton struct {
    }
    
    var ins *singleton = &singleton{}
    
    func GetInsOr() *singleton {
        return ins
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    因为实例是在包被导入时初始化的,所以如果初始化耗时,会导致程序加载时间比较长。

    1.1.2 懒汉方式

    懒汉方式是开源项目中使用最多的,但它的缺点是非并发安全,在实际使用时需要加锁。以下是懒汉方式不加锁的一个实现:

    package singleton
    
    type singleton struct {
    }
    
    var ins *singleton
    
    func GetInsOr() *singleton {
        if ins == nil {
            ins = &singleton{}
        }
        
        return ins
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到,在创建 ins 时,如果 ins==nil,就会再创建一个 ins 实例,这时候单例就会有多个实例。

    为了解决懒汉方式非并发安全的问题,需要对实例进行加锁,下面是带检查锁的一个实现:

    import "sync"
    
    type singleton struct {
    }
    
    var ins *singleton
    var mu sync.Mutex
    
    func GetIns() *singleton {
      // go 的双重检测
      if ins == nil {
        mu.Lock()
        if ins == nil {
          ins = &singleton{}
        }
        mu.Unlock()
      }
      return ins
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    上述代码只有在创建时才会加锁,既提高了代码效率,又保证了并发安全。

    1.1.3 推荐方式

    除了饿汉方式和懒汉方式,在 Go 开发中,还有一种更优雅的实现方式,我建议你采用这种方式,代码如下:

    package singleton
    
    import (
        "sync"
    )
    
    type singleton struct {
    }
    
    var ins *singleton
    var once sync.Once
    
    func GetInsOr() *singleton {
        once.Do(func() {
            ins = &singleton{}
        })
        return ins
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    使用 once.Do 可以确保 ins 实例全局只被创建一次,once.Do 函数还可以确保当同时有多个创建动作时,只有一个创建动作在被执行。

    1.2 工厂模式

    工厂模式是面向对象编程中的常用模式。Go 中的结构体,可以理解为面向对象编程中的类,例如 Person 结构体(类)实现了 Greet 方法。

    type Person struct {
      Name string
      Age int
    }
    
    func (p Person) Greet() {
      fmt.Printf("Hi! My name is %s", p.Name)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    有了 Person “类”,就可以创建 Person 实例。我们可以通过简单工厂模式、抽象工厂模式、工厂方法模式这三种方式,来创建一个 Person 实例。

    1.2.1 简单工厂模式

    简单工厂模式是最常用、最简单的。它就是一个接受一些参数,然后返回 Person 实例的函数:

    type Person struct {
      Name string
      Age int
    }
    
    func (p Person) Greet() {
      fmt.Printf("Hi! My name is %s", p.Name)
    }
    
    func NewPerson(name string, age int) *Person {
      return &Person{
        Name: name,
        Age: age,
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    p := &Person{}
    
    • 1

    这种创建实例的方式相比,简单工厂模式可以确保我们创建的实例具有需要的参数,进而保证实例的方法可以按预期执行。例如,通过 NewPerson 创建 Person 实例时,可以确保实例的 nameage 属性被设置。

    1.2.2 抽象工厂模式

    它和简单工厂模式的唯一区别,就是它返回的是接口而不是结构体。通过返回接口,可以在你不公开内部实现的情况下,让调用者使用你提供的各种功能,例如:

    type Person interface {
      Greet()
    }
    
    type person struct {
      name string
      age int
    }
    
    func (p person) Greet() {
      fmt.Printf("Hi! My name is %s", p.name)
    }
    
    // Here, NewPerson returns an interface, and not the person struct itself
    func NewPerson(name string, age int) Person {
      return person{
        name: name,
        age: age,
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    上面这个代码,定义了一个不可导出的结构体 person,在通过 NewPerson 创建实例的时候返回的是接口,而不是结构体。

    另外,在使用简单工厂模式和抽象工厂模式返回实例对象时,都可以返回指针。例如,简单工厂模式可以这样返回实例对象:

    return &Person{
      Name: name,
      Age: age
    }
    
    • 1
    • 2
    • 3
    • 4

    抽象工厂模式可以这样返回实例对象:

    return &person{
      name: name,
      age: age
    }
    
    • 1
    • 2
    • 3
    • 4

    在实际开发中,建议返回非指针的实例,因为我们主要是想通过创建实例,调用其提供的方法,而不是对实例做更改。如果需要对实例做更改,可以实现 SetXXX 的方法。通过返回非指针的实例,可以确保实例的属性,避免属性被意外 / 任意修改。

    1.2.3 工厂方法模式

    在简单工厂模式中,依赖于唯一的工厂对象,如果我们需要实例化一个产品,就要向工厂中传入一个参数,获取对应的对象;如果要增加一种产品,就要在工厂中修改创建产品的函数。这会导致耦合性过高,这时我们就可以使用工厂方法模式。

    在工厂方法模式中,依赖工厂函数,我们可以通过实现工厂函数来创建多种工厂,将对象创建从由一个对象负责所有具体类的实例化,变成由一群子类来负责对具体类的实例化,从而将过程解耦。

    type Person struct {
      name string
      age int
    }
    
    func NewPersonFactory(age int) func(name string) Person {
      return func(name string) Person {
        return Person{
          name: name,
          age: age,
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    然后,我们可以使用此功能来创建具有默认年龄的工厂:

    newBaby := NewPersonFactory(1)
    baby := newBaby("john")
    
    newTeenager := NewPersonFactory(16)
    teen := newTeenager("jill")
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2. 结构型模式

    结构型模式的特点是关注类和对象的组合。

    2.1 策略模式

    策略模式定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。

    在项目开发中,我们经常要根据不同的场景,采取不同的措施,也就是不同的策略。比如,假设我们需要对 a、b 这两个整数进行计算,根据条件的不同,需要执行不同的计算方式。我们可以把所有的操作都封装在同一个函数中,然后通过 if … else … 的形式来调用不同的计算方式,这种方式称之为硬编码。

    在实际应用中,随着功能和体验的不断增长,我们需要经常添加 / 修改策略,这样就需要不断修改已有代码,不仅会让这个函数越来越难维护,还可能因为修改带来一些 bug。所以为了解耦,需要使用策略模式,定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法(即策略)。

    package strategy
    
    // 策略模式
    
    // 定义一个策略类
    type IStrategy interface {
      do(int, int) int
    }
    
    // 策略实现:加
    type add struct{}
    
    func (*add) do(a, b int) int {
      return a + b
    }
    
    // 策略实现:减
    type reduce struct{}
    
    func (*reduce) do(a, b int) int {
      return a - b
    }
    
    // 具体策略的执行者
    type Operator struct {
      strategy IStrategy
    }
    
    // 设置策略
    func (operator *Operator) setStrategy(strategy IStrategy) {
      operator.strategy = strategy
    }
    
    // 调用策略中的方法
    func (operator *Operator) calculate(a, b int) int {
      return operator.strategy.do(a, b)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    在上述代码中,我们定义了策略接口 IStrategy,还定义了 addreduce 两种策略。最后定义了一个策略执行者,可以设置不同的策略,并执行,例如:

    func TestStrategy(t *testing.T) {
      operator := Operator{}
    
      operator.setStrategy(&add{})
      result := operator.calculate(1, 2)
      fmt.Println("add:", result)
    
      operator.setStrategy(&reduce{})
      result = operator.calculate(2, 1)
      fmt.Println("reduce:", result)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到,我们可以随意更换策略,而不影响 Operator 的所有实现。

    2.2 模版模式

    简单来说,模板模式就是将一个类中能够公共使用的方法放置在抽象类中实现,将不能公共使用的方法作为抽象方法,强制子类去实现,这样就做到了将一个类作为一个模板,让开发者去填充需要填充的地方。

    
    package template
    
    import "fmt"
    
    type Cooker interface {
      fire()
      cooke()
      outfire()
    }
    
    // 类似于一个抽象类
    type CookMenu struct {
    }
    
    func (CookMenu) fire() {
      fmt.Println("开火")
    }
    
    // 做菜,交给具体的子类实现
    func (CookMenu) cooke() {
    }
    
    func (CookMenu) outfire() {
      fmt.Println("关火")
    }
    
    // 封装具体步骤
    func doCook(cook Cooker) {
      cook.fire()
      cook.cooke()
      cook.outfire()
    }
    
    type XiHongShi struct {
      CookMenu
    }
    
    func (*XiHongShi) cooke() {
      fmt.Println("做西红柿")
    }
    
    type ChaoJiDan struct {
      CookMenu
    }
    
    func (ChaoJiDan) cooke() {
      fmt.Println("做炒鸡蛋")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    使用

    func TestTemplate(t *testing.T) {
      // 做西红柿
      xihongshi := &XiHongShi{}
      doCook(xihongshi)
    
      fmt.Println("\n=====> 做另外一道菜")
      // 做炒鸡蛋
      chaojidan := &ChaoJiDan{}
      doCook(chaojidan)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3. 行为型模式

    行为型模式的特点是关注对象之间的通信。

    3.1 代理模式

    代理模式可以为另一个对象提供一个替身或者占位符,以控制对这个对象的访问。

    
    package proxy
    
    import "fmt"
    
    type Seller interface {
      sell(name string)
    }
    
    // 火车站
    type Station struct {
      stock int //库存
    }
    
    func (station *Station) sell(name string) {
      if station.stock > 0 {
        station.stock--
        fmt.Printf("代理点中:%s买了一张票,剩余:%d \n", name, station.stock)
      } else {
        fmt.Println("票已售空")
      }
    
    }
    
    // 火车代理点
    type StationProxy struct {
      station *Station // 持有一个火车站对象
    }
    
    func (proxy *StationProxy) sell(name string) {
      if proxy.station.stock > 0 {
        proxy.station.stock--
        fmt.Printf("代理点中:%s买了一张票,剩余:%d \n", name, proxy.station.stock)
      } else {
        fmt.Println("票已售空")
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    上述代码中,StationProxy 代理了 Station,代理类中持有被代理类对象,并且和被代理类对象实现了同一接口。

    3.2 选项模式

    选项模式也是 Go 项目开发中经常使用到的模式,例如,grpc/grpc-goNewServer 函数,uber-go/zap 包的 New 函数都用到了选项模式。

    使用选项模式,我们可以创建一个带有默认值的 struct 变量,并选择性地修改其中一些参数的值。

    Python 语言中,创建一个对象时,可以给参数设置默认值,这样在不传入任何参数时,可以返回携带默认值的对象,并在需要时修改对象的属性。这种特性可以大大简化开发者创建一个对象的成本,尤其是在对象拥有众多属性时。

    而在 Go 语言中,因为不支持给参数设置默认值,为了既能够创建带默认值的实例,又能够创建自定义参数的实例,不少开发者会通过以下两种方法来实现:

    第一种方法,我们要分别开发两个用来创建实例的函数,一个可以创建带默认值的实例,一个可以定制化创建实例。

    package options
    
    import (
      "time"
    )
    
    const (
      defaultTimeout = 10
      defaultCaching = false
    )
    
    type Connection struct {
      addr    string
      cache   bool
      timeout time.Duration
    }
    
    // NewConnect creates a connection.
    func NewConnect(addr string) (*Connection, error) {
      return &Connection{
        addr:    addr,
        cache:   defaultCaching,
        timeout: defaultTimeout,
      }, nil
    }
    
    // NewConnectWithOptions creates a connection with options.
    func NewConnectWithOptions(addr string, cache bool, timeout time.Duration) (*Connection, error) {
      return &Connection{
        addr:    addr,
        cache:   cache,
        timeout: timeout,
      }, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    使用这种方式,创建同一个 Connection 实例,却要实现两个不同的函数,实现方式很不优雅。

    另外一种方法相对优雅些。我们需要创建一个带默认值的选项,并用该选项创建实例:

    package options
    
    import (
      "time"
    )
    
    const (
      defaultTimeout = 10
      defaultCaching = false
    )
    
    type Connection struct {
      addr    string
      cache   bool
      timeout time.Duration
    }
    
    type ConnectionOptions struct {
      Caching bool
      Timeout time.Duration
    }
    
    func NewDefaultOptions() *ConnectionOptions {
      return &ConnectionOptions{
        Caching: defaultCaching,
        Timeout: defaultTimeout,
      }
    }
    
    // NewConnect creates a connection with options.
    func NewConnect(addr string, opts *ConnectionOptions) (*Connection, error) {
      return &Connection{
        addr:    addr,
        cache:   opts.Caching,
        timeout: opts.Timeout,
      }, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    使用这种方式,虽然只需要实现一个函数来创建实例,但是也有缺点:为了创建 Connection 实例,每次我们都要创建 ConnectionOptions,操作起来比较麻烦。

    以下代码通过选项模式实现上述功能:

    package options
    
    import (
      "time"
    )
    
    type Connection struct {
      addr    string
      cache   bool
      timeout time.Duration
    }
    
    const (
      defaultTimeout = 10
      defaultCaching = false
    )
    
    type options struct {
      timeout time.Duration
      caching bool
    }
    
    // Option overrides behavior of Connect.
    type Option interface {
      apply(*options)
    }
    
    type optionFunc func(*options)
    
    func (f optionFunc) apply(o *options) {
      f(o)
    }
    
    func WithTimeout(t time.Duration) Option {
      return optionFunc(func(o *options) {
        o.timeout = t
      })
    }
    
    func WithCaching(cache bool) Option {
      return optionFunc(func(o *options) {
        o.caching = cache
      })
    }
    
    // Connect creates a connection.
    func NewConnect(addr string, opts ...Option) (*Connection, error) {
      options := options{
        timeout: defaultTimeout,
        caching: defaultCaching,
      }
    
      for _, o := range opts {
        o.apply(&options)
      }
    
      return &Connection{
        addr:    addr,
        cache:   options.caching,
        timeout: options.timeout,
      }, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    选项模式有很多优点,例如:支持传递多个参数,并且在参数发生变化时保持兼容性;支持任意顺序传递参数;支持默认值;方便扩展;通过 WithXXX 的函数命名,可以使参数意义更加明确,等等。

    不过,为了实现选项模式,我们增加了很多代码,所以在开发中,要根据实际场景选择是否使用选项模式。选项模式通常适用于以下场景:

    • 结构体参数很多,创建结构体时,我们期望创建一个携带默认值的结构体变量,并选择性修改其中一些参数的值。
    • 结构体参数经常变动,变动时我们又不想修改创建实例的函数。例如:结构体新增一个 retry 参数,但是又不想在 NewConnect 入参列表中添加 retry int 这样的参数声明。

    如果结构体参数比较少,可以慎重考虑要不要采用选项模式。

    4. 总结

    总结
    原文:https://time.geekbang.org/column/article/386238

  • 相关阅读:
    js中的设计模式之代理模式
    react数据管理之setState与Props
    100天精通Python(数据分析篇)——第54天:Series对象大总结
    SYN泛洪攻击程序设计
    vben admin 中 BasicTable 组件 useTable 的使用
    众贷贡献的DOT已经解锁,各类应用也已准备就绪
    Linux基础——ELK Stack
    怎样让健康码截图合并一张图片_健康码拼图
    python 娣卞害绁炵粡缃戠粶,python 缃戠粶鍒嗘瀽
    AM5-DB低压备自投装置在河北冠益荣信科技公司洞庭变电站工程中的应用
  • 原文地址:https://blog.csdn.net/wohu1104/article/details/123240851