Golang 常见设计模式之选项模式

3A网络
• 阅读 157

Golang 常见设计模式之选项模式

熟悉 Python 开发的同学都知道,Python 有默认参数的存在,使得我们在实例化一个对象的时候,可以根据需要来选择性的覆盖某些默认参数,以此来决定如何实例化对象。当一个对象有多个默认参数时,这个特性非常好用,能够优雅地简化代码。

而 Go 语言从语法上是不支持默认参数的,所以为了实现既能通过默认参数创建对象,又能通过传递自定义参数创建对象,我们就需要通过一些编程技巧来实现。对于这些程序开发中的常见问题,软件行业的先行者们总结了许多解决常见场景编码问题的最佳实践,这些最佳实践后来成为了我们所说的设计模式。其中选项模式在 Go 语言开发中会经常用到。

通常我们有以下三种方法来实现通过默认参数创建对象,以及通过传递自定义参数创建对象:

  • 使用多个构造函数
  • 默认参数选项
  • 选项模式

通过多构造函数实现

第一种方式是通过多构造函数实现,下面是一个简单例子:

package main

import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

func NewServer() *Server {
    return &Server{
        Addr: defaultAddr,
        Port: defaultPort,
    }
}

func NewServerWithOptions(addr string, port int) *Server {
    return &Server{
        Addr: addr,
        Port: port,
    }
}

func main() {
    s1 := NewServer()
    s2 := NewServerWithOptions("localhost", 8001)
    fmt.Println(s1)  // &{127.0.0.1 8000}
    fmt.Println(s2)  // &{localhost 8001}
}

这里我们为 Server 结构体实现了两个构造函数:

  • NewServer:无需传递参数即可直接返回 Server 对象
  • NewServerWithOptions :需要传递 addr 和 port 两个参数来构造 Server 对象

如果通过默认参数创建的对象即可满足需求,不需要对 Server 进行定制时,我们可以使用 NewServer 来生成对象(s1)。而如果需要对 Server 进行定制时,我们则可以使用 NewServerWithOptions 来生成对象(s2)。

通过默认参数选项实现

另外一种实现默认参数的方案,是为要生成的结构体对象定义一个选项结构体,用来生成要创建对象的默认参数,代码实现如下:

package main

import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

type ServerOptions struct {
    Addr string
    Port int
}

func NewServerOptions() *ServerOptions {
    return &ServerOptions{
        Addr: defaultAddr,
        Port: defaultPort,
    }
}

func NewServerWithOptions(opts *ServerOptions) *Server {
    return &Server{
        Addr: opts.Addr,
        Port: opts.Port,
    }
}

func main() {
    s1 := NewServerWithOptions(NewServerOptions())
    s2 := NewServerWithOptions(&ServerOptions{
        Addr: "localhost",
        Port: 8001,
    })
    fmt.Println(s1)  // &{127.0.0.1 8000}
    fmt.Println(s2)  // &{localhost 8001}
}

我们为 Server 结构体专门实现了一个 ServerOptions 用来生成默认参数,调用 NewServerOptions 函数即可获得默认参数配置,构造函数 NewServerWithOptions 接收一个 *ServerOptions 类型作为参数。所以我们可以通过以下两种方式来完成功能:

  • 直接将调用 NewServerOptions 函数的返回值传递给 NewServerWithOptions 来实现通过默认参数生成对象(s1)
  • 通过手动构造 ServerOptions 配置来生成定制对象(s2)

通过选项模式实现

以上两种方式虽然都能够完成功能,但却有以下缺点:

  • 通过多构造函数实现的方案需要我们在实例化对象时分别调用不同的构造函数,代码封装性不强,会给调用者增加使用负担。
  • 通过默认参数选项实现的方案需要我们预先构造一个选项结构,当使用默认参数生成对象时代码看起来比较冗余。

而选项模式可以让我们更为优雅地解决这个问题。代码实现如下:

package main

import "fmt"

const (
    defaultAddr = "127.0.0.1"
    defaultPort = 8000
)

type Server struct {
    Addr string
    Port int
}

type ServerOptions struct {
    Addr string
    Port int
}

type ServerOption interface {
    apply(*ServerOptions)
}

type FuncServerOption struct {
    f func(*ServerOptions)
}

func (fo FuncServerOption) apply(option *ServerOptions) {
    fo.f(option)
}

func WithAddr(addr string) ServerOption {
    return FuncServerOption{
        f: func(options *ServerOptions) {
            options.Addr = addr
        },
    }
}

func WithPort(port int) ServerOption {
    return FuncServerOption{
        f: func(options *ServerOptions) {
            options.Port = port
        },
    }
}

func NewServer(opts ...ServerOption) *Server {
    options := ServerOptions{
        Addr: defaultAddr,
        Port: defaultPort,
    }

    for _, opt := range opts {
        opt.apply(&options)
    }

    return &Server{
        Addr: options.Addr,
        Port: options.Port,
    }
}

func main() {
    s1 := NewServer()
    s2 := NewServer(WithAddr("localhost"), WithPort(8001))
    s3 := NewServer(WithPort(8001))
    fmt.Println(s1)  // &{127.0.0.1 8000}
    fmt.Println(s2)  // &{localhost 8001}
    fmt.Println(s3)  // &{127.0.0.1 8001}
}

乍一看我们的代码复杂了很多,但其实调用构造函数生成对象的代码复杂度是没有改变的,只是定义上的复杂。

我们定义了 ServerOptions 结构体用来配置默认参数。因为 Addr 和 Port 都有默认参数,所以 ServerOptions 的定义和 Server 定义是一样的。但有一定复杂性的结构体中可能会有些参数没有默认参数,必须让用户来配置,这时 ServerOptions 的字段就会少一些,大家可以按需定义。

同时,我们还定义了一个 ServerOption 接口和实现了此接口的 FuncServerOption 结构体,它们的作用是让我们能够通过 apply 方法为 ServerOptions 结构体单独配置某项参数。

我们可以分别为每个默认参数都定义一个 WithXXX 函数用来配置参数,如这里定义的 WithAddr 和 WithPort ,这样用户就可以通过调用 WithXXX 函数来定制需要覆盖的默认参数。

此时默认参数定义在构造函数 NewServer 中,构造函数的接收一个不定长参数,类型为 ServerOption,在构造函数内部通过一个 for 循环调用每个传进来的 ServerOption 对象的 apply 方法,将用户配置的参数依次赋值给构造函数内部的默认参数对象 options 中,以此来替换默认参数,for 循环执行完成后,得到的 options 对象将是最终配置,将其属性依次赋值给 Server 即可生成新的对象。

总结

通过 s2 和 s3 的打印结果可以发现,使用选项模式实现的构造函数更加灵活,相较于前两种实现,选项模式中我们可以自由的更改其中任意一项或多项默认配置。

虽然选项模式确实会多写一些代码,但多数情况下这都是值得的。比如 Google 的 gRPC 框架 Go 语言实现中创建 gRPC server 的构造函数 NewServer 就使用了选项模式,感兴趣的同学可以看下其源码的实现思想其实和这里的示例程序如出一辙、感兴趣的朋友可以在3A服务器上部署相关的环境,进行试验。以上就是我关于 Golang 选项模式的一点经验,希望今天的分享能够给你带来一些帮助。

点赞
收藏
评论区
推荐文章
九路 九路
1年前
一篇文章彻底弄懂go语言方法的本质
Go语言不支持经典的面向对象语法元素,比如:类、对象、继承等。但Go语言也有方法(method)。和函数相比,Go语言中的方法在声明形式上仅仅多了一个参数,Go称之为receiver参数。而receiver参数正是方法与类型之间的纽带。Go方法的一般声明形式如下:gofunc(receiverT/T)MethodName(参数列表)
Wesley13 Wesley13
1年前
JAVA设计模式之单例设计模式
    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。  在JAVA中实现单例,必须了解JAVA内存机制,JAVA中实例对象存在于堆内存中,若要实现单例,必须满足两个条件:  1.限制类实例化对象。即只能产生一个对象。
Stella981 Stella981
1年前
K8s中,tomcat的一部分jvm参数,如何通过env环境变量传递?
这两天解决的一个需求:如果用户没有在deployment中设置env参数,则tomcat默认使用1G左右的内存;如果用户在deployment中提供了jvm参数,则tomcat将这部分的参数,覆盖掉默认的jvm参数。这个实现思路是ok的,但在实现shell脚本时,老司机也遇到了新问题:如何判断一个有空格的环境变量是否存在?最后通过百度搞定
Wesley13 Wesley13
1年前
Unity C# 设计模式(二)简单工厂模式
定义:简单工厂模式是属于创建型模式,又叫做静态工厂方法(StaticFactoryMethod)模式,但不属于23种GOF设计模式之一。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该
Wesley13 Wesley13
1年前
(面试常问)4种单例设计模式的总结(内含代码以及分析)
单例设计模式:  单例模式,是一种常见的软件设计模式.在它的核心结构中只包含了一个被称为单例的特殊类.通过单例模式可以保证系统中只有该类的一个实例对象.优点:  实例控制:单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问的是唯一的实例   灵活性:因为类控制了实例化过程,所以类可以很灵活的更改实
Wesley13 Wesley13
1年前
C#设计模式 —— 工厂模式
。  工厂模式同样是项目中最常用的设计模式,工厂模式中又分为简单工厂,工厂方法,抽象工厂。下面我们由简单的开始逐一介绍。1.简单工厂模式  简单工厂又被称为静态工厂,在设计模式中属于创建型模式。主要解决的问题是封装了实例化的过程,通过传入参数来获不同实例。下面我们举一个项目中可能会用到的例子。  假设我们程序的数据保存在几个不同
Wesley13 Wesley13
1年前
Java中jdk代理和cglib代理
代理模式给某一个对象提供一个代理,并由代理对象控制对原对象的引用。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。在Java中代理模式从实现方式上可以分为两个类别:静态代理和动态代理静态代理:也就是我们学习设计模式之代理模式时常见的事例,具体不在赘述,参见:
Wesley13 Wesley13
1年前
Java设计模式之命令模式
介绍命令模式是一种行为型设计模式。在命令模式中,所有的请求都会被包装成为一个对象。参考了一下其他关于命令模式的文章,其中有谈到说是可以用不同的请求对客户进行参数化。对这句话的理解是,因为将请求封装成为对象,所以客户的所有操作,其实就是多个命令类的对象而已,即参数化了。命令模式的最大的特点就是将请求的调用者与请求的最终执行者进行了解
3A网络 3A网络
5个月前
Golang 常见设计模式之装饰模式
Golang常见设计模式之装饰模式想必只要是熟悉Python的同学对装饰模式一定不会陌生,这类Python从语法上原生支持的装饰器,大大提高了装饰模式在Python中的应用。尽管Go语言中装饰模式没有Python中应用的那么广泛,但是它也有其独到的地方。接下来就一起看下装饰模式在Go语言中的应用。简单装饰器我们通过一个简单的例子来
3A网络 3A网络
5个月前
Golang 常见设计模式之单例模式
之前我们已经看过了Golang常见设计模式中的装饰和选项模式,今天要看的是Golang设计模式里最简单的单例模式。单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。Go语言实现单例模式的方法有很多种,下面我们就一起来看一下。饿汉式饿汉式实现单例模式非