Go语言常见的接口设计模式

459 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情

接口背景

接口设计的一些原则:

  • 在实际真正需要的时候才对程序进行抽象,不要为了抽象而抽象
  • 接口的副作用是提高的代码的抽象程度,降低运行效率和可读性

好的设计模式的特点:

  • 正交:解耦性,对输入的参数,各自独立,一个改变不会影响其他的。

静态骨架结构的两种组合方式

垂直组合

interface、struct的匿名字段嵌入 垂直组合更多应用在新类型的定义方面,可以达到方法实现的复用、接口定义重用等目的。

水平组合

通过函数传入接口,实现不同接口间的连接,因为使用的是接口类型的参数,基于最小接口原则,提供了很好的拓展性和正交性。 例如下面的代码:

func Save(w io.Writer, data []byte) error

所有实现io.Writer接口的实例都可以调用Save方法,例如io、file、string buffer等

常见的接口设计模式

基本模式

基本的水平组合:接受接口类型参数的函数或方法

func YourFuncName(param YourInterfaceType)

image.png

这里注意:

  • 参数的接口类型是传入实体接口类型的子集,相当于可以用共同行为的对象进行替换,像关节一样灵活。
  • 满足:依赖抽象(DIP)、里氏替换原则(LSP)、接口隔离(ISP)等代码设计原则

创建模式

接受接口,返回结构体(Accept interfaces, return structs),常用于接口类型字段的结构体的实例化。

type M struct{
     xxx xxx
     reader io.Reader
}

fun New(reader io.Reader,xxx xxx) *M{
    return &M{reader,xxx}
}

包装器模式

在基本模式的基础上,返回值的类型与参数类型相同。实现对输入参数的类型的包装,并在不改变被包装类型(输入参数类型)的定义的情况下,返回具备新功能特性的、实现相同接口类型的新类型。包装器多用于对输入数据的过滤、变换等操作。

下面演示了,对io.Reader进行包装,在调用其Read方法时,添加了长度拦截的功能(不破坏原方法的签名)。

func YourWrapperFunc(param YourInterfaceType) YourInterfaceType
// 小栗子
type M struct{
     xxx xxx
     reader io.Reader
}

type LimitRead struct{
     r io.Reader
     limit int
}
func (r *LimitRead)Read() string{
    s:=r.r.Read()
    return s[:r.limit]
}

fun New(reader io.Reader,n int) io.Reader{
    return &LimitRead{reader,n}
}

可以将多个接受同一接口类型参数的包装函数组合成一条链来调用

YourWrapperFunc1(YourWrapperFunc2(YourWrapperFunc3(...)))

适配器模式

适配器函数类型是一个辅助水平组合实现的“工具”类型。这里我要再强调一下,它是一个类型。它可以将一个满足特定函数签名的普通函数,显式转换成自身类型的实例,转换后的实例同时也是某个接口类型的实现者。

http源码的示例:

func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greetings))
}


// $GOROOT/src/net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

上述代码http.ListenAndServe需要一个Handler类型的参数,会调用这个类型的ServeHTTP方法。http.HandlerFunc就是一个适配器类型,提供类型转化,可以将任何符合这个函数签名的函数通过类型转化传给http.ListenAndServe,然后可以后续调用。

emmm,为啥不直接传这个greetings函数,http直接调用greetings????

中间件(Middleware)

在 Go Web 编程中,“中间件”常常指的是一个实现了 http.Handler 接口的 http.HandlerFunc 类型实例。实质上,这里的中间件就是包装模式和适配器模式结合的产物。 小栗子:

func validateAuth(s string) error {
    if s != "123456" {
        return fmt.Errorf("%s", "bad auth token")
    }
    return nil
}

func greetings(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func logHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)
        h.ServeHTTP(w, r)
    })
}

func authHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        err := validateAuth(r.Header.Get("auth"))
        if err != nil {
            http.Error(w, "bad auth param", http.StatusUnauthorized)
            return
        }
        h.ServeHTTP(w, r)
    })

}

func main() {
    http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings))))
}

上述http.HandlerFunc(greetings)就是适配器模式,下面在外边套了两个中间件(输入和输出为适配器模式类型的函数)logHandler和authHandler,分别实现了打印时间和身份验证,相当于对适配器打了两个补丁。 综上所述,中间件就是适配器的包装器。