适配器模式-Go

适配器模式

最常用的结构模式之一是适配器模式。就像在现实生活中,你有插头适配器和螺栓适配器,在Go中,适配器将允许我们使用一些在开始时不是为特定任务而构建的东西。

描述

适配器模式非常有用,例如,当一个接口过时,无法轻松或快速地替换它时。相反,您可以创建一个新接口来处理应用程序的当前需求,该接口在幕后使用旧接口的实现。

适配器还帮助我们在应用程序中遵循开闭原则,使它们更加可预测。它们还允许我们编写使用一些我们无法修改的基础代码。

目标

适配器设计模式将帮助您满足最初不兼容的两部分代码的需要。在决定适配器模式是否适合您的问题时,要记住这一点——两个不兼容但必须协同工作的接口是适配器模式的最佳候选接口(但它们也可以使用Facade模式)。

使用与适配器对象不兼容的接口

例如,我们有一个老旧的Printer接口和一个新的。新接口的调用者不希望收到旧接口的签名,因此需要一个适配器,以便用户在必要时仍可以使用旧实现(例如,使用一些遗留代码)。

要求与验收标准

有一个名为LegacyPrinter的旧接口和一个名为ModernPrinter的新接口,创建一个实现ModernPrinter接口的结构,并且可以使用LegacyPrinter接口,如一下步骤所述:

  1. 创建实现ModernPrinter接口的适配器对象
  2. 新适配器对象必须包含LegacyPrinter接口的实例
  3. 当使用ModernPrinter时,必须私下调用,并且在前面加上文本适配器

实现

首先是LegacyPrinter的实现:

type LegacyPrinter interface { 
    Print(s string) string
} 

type MyLegacyPrinter struct {}

func (m *MyLegacyPrinter) Print(s string) (newMsg string) { //此处在方法定义处定义了返回值变量
    newMsg = fmt.Sprintf("Legacy Printer: %s\n", s) 
    println(newMsg) 
    return //直接返回无需传递变量,因为在函数头一行已经定义
}

首先LegacyPrinter接口有一个Print方法,接受一个字符串,返回一段消息。MyLegacyPrinter结构体实现了这接个口,修改了传递的字符串,打印并返回一个带前缀的字符串。

现在定义需要适配的新接口:

type ModernPrinter interface {
    PrintStored() string
}

在这种情况下,新的PrintStored()方法不接受任何字符串作为参数,因此需要提前在实现类中储存起来。我们把适配器类型命名为PrinterAdapter接口:

type PrinterAdapter struct {
    OldPrinter LegacyPrinter
    Msg string
}

func (p *PrinterAdapter) PrintStored() (newMsg string) {
    return
}

前面提到,PrinterAdapter适配器必须有一个存储待打印字符串的字段。也必须有一个字段存储LegacyPrinter实例。

扫描二维码关注公众号,回复: 9552717 查看本文章

现在我们写一个小测试:

func TestAdapter(t *testing.T) {
    msg := "Hello World!"
    adapter := PrinterAdapter{
        OldPrinter: &MyLegacyPrinter{},
        //此处创建一个MyLegacyPrinter实例,赋给OldPrinter
        Msg: msg,
    }
    returnedMsg := adapter.PrintStored()
    if returnedMsg != "Legacy Printer: Adapter: Hello World!\n" {
        t.Errorf("Message didn't match: %s\n", returnedMsg)
    }
    adapter = PrinterAdapter{
        OldPrinter: nil, 
        //此处传递一个空值给OldPrinter,期望按原样输出
        Msg: msg,
    } 
    returnedMsg = adapter.PrintStored()
    if returnedMsg != "Hello World!" { 
        t.Errorf("Message didn't match: %s\n", returnedMsg) 
    }
}

接下来需要重用PrinterAdapter中存储的MyLegacyPrinter

type PrinterAdapter struct {
    OldPrinter LegacyPrinter
    Msg string
}

func(p *PrinterAdapter) PrintStored() (newMsg string) {
    if p.OldPrinter != nil{
        newMsg = fmt.Sprintf("Adapter: %s", p.Msg)
        newMsg = p.OldPrinter.Print(newMsg)
    } else {
        newMsg = p.Msg
    }
    return
}

PrintStored方法中,首先检查是否有LegacyPrinter实例。在此例中,我们使用存储的消息和适配器前缀组成一个新字符串,将其存储在返回变量(称为newMsg)中。然后我们使用指向MyLegacyPrinter结构的指针来使用LegacyPrinter接口打印合成的消息。如果LegacyPrinter实例不存在,就直接返回原始值。

源码见 https://github.com/ricardoliu404/go-design-patterns/tree/master/structural/adapter

Go源代码中的适配器模式举例

在Go语言的源代码中你可以找到很多适配器实现。著名的http.Handler接口就是一个非常有趣的适配器实现。Go语言中的一个非常简单的Hello World服务器是这么实现的:

package main
import ( 
    "fmt" 
    "log"
    "net/http" 
)
type MyServer struct{ 
    Msg string
}
func (m *MyServer) ServeHTTP(w http.ResponseWriter,r *http.Request){ 
    fmt.Fprintf(w, "Hello, World")
} 
func main() {
    server := &MyServer{ 
        Msg:"Hello, World",
    } 
    http.Handle("/", server)
    log.Fatal(http.ListenAndServe(":8080", nil)) 
}

HTTP包有一个函数Handle(类似Java中的静态方法)接受两个参数——一个字符串代表路径,还有一个Handler接口。Handler接口看起来像下面这样:

type Handler interface {
    ServeHTTP (ResponseWriter, *Request)
}

我们需要实现一个ServeHTTP方法,服务端的HTTP链接会运行处理上下文。但是还有一个方法HandlerFunc允许你定义一些终端表现:

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
        fmt.Fprintf(w, "Hello, World")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

HandleFunc函数实际上是适配器的一部分,用于将函数直接用作ServeHTTP实现。再慢慢读最后一句话-你能猜出是怎么做到的吗?

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

我们可以像定义结构一样定义一个函数类型。我们将此函数类型设置为实现ServeHTTP方法。最后,从ServeHTTP函数中,我们调用接收器本身f(w,r)

您必须考虑Go的隐式接口实现。当我们定义像func(ResponseWriter,*Request)这样的函数时,它被隐式地识别为HandlerFunc。因为HandleFunc函数实现了处理程序接口,所以我们的函数也隐式地实现了处理程序接口。这听起来你熟悉吗?如果A=BB=C,那么A=C.隐式实现提供了很大的灵活性和能力,但是您也必须小心,因为您不知道某个方法或函数是否正在实现某个接口,而该接口可能会引发不受欢迎的行为。

我们可以在Go的源代码中找到更多的例子。io包还有一个使用管道的强大示例。Linux中的管道是一种流机制,它接受输入上的某些内容,并在输出上输出其他内容。io包有两个接口,在Go的源代码中到处都使用—io.Readerio.Writer接口:

type Reader interface { 
    Read(p []byte) (n int, err error) 
} 

type Writer interface { 
    Write(p []byte) (n int, err error) 
}

我们在任何地方都使用io.Reader,例如,当您使用os.open file打开一个文件时,它返回一个文件,实际上,该文件实现了io.Reader接口。为什么有用?假设您编写了一个计数器结构,它会从您提供的数字到零计数:

type Counter struct {}
func (f *Counter) Count(n uint64) uint64 { 
    if n == 0 {
        println(strconv.Itoa(0)) 
        return 0
    } 
    cur := n
    println(strconv.FormatUint(cur, 10)) 
    return f.Count(n - 1)
}
//如果输入为3
//输出为
/*
 *3
 *2
 *1
*/

简单的几何学递归。但如果我们想要写到文件里呢?则需要实现这个函数一遍。如果我们需要输出到控制台呢?还要实现一遍。我们需要用io.Writer吧这个函数结构化一些:

type Counter struct {
    Writer io.Writer
}

func (f *Counter) count(n uint64) uint64{
    if n == 0 {
        f.Writer.Write([]byte(strconv.Itoa(0) + "\n"))
        return 0
    }
    cur := n
    f.Writer.Write([]byte(strconv.FormatUint(cur, 10) + "\n"))
    return f.Count(n - 1)
}

现在我们有了一个io.WriterWriter字段中。这样一来,我们可以这样创建计数器c := Counter{os.Stdout},我们就会获得控制台的Writer。但还是没有解决我们想要把倒数输出到许多控制台。我们可以写一个新的适配器,使用一个Pipe()对读方和写方建立连接。这样一来,就可以解决ReaderWriter接口不兼容的问题。

实际上,我们不需要编写适配器——Go的io库在io.Pipe()中为我们提供了一个适配器。管道将允许我们将Reader转换为WriterPipe()方法将提供一个Writer(管道的入口)和一个Reader(出口)供我们使用。因此,让我们创建一个管道,并将提供的writer分配给前面示例的计数器:

pipeReader, pipeWriter := io.Pipe()
defer pw.Close()
defer pr.Close()

counter := Counter{
    Writer: pipeWriter,
}

现在我们有了一个Reader接口,在那里我们以前有一个Writer。我们在哪里可以使用Readerio.TeeReader函数帮助我们将数据流从读接口复制到写接口,并返回一个新的Reader,您仍然可以使用该Reader再次将数据流传输到第二个Writer。因此,我们将数据从同一个Reader流到两个Writer-fileStdOut

tee := io.TeeReader(pipeReader, file)

现在我们将会往向TeeReader方法传递的文件中写入,我们还需要向控制台打印。io.Copy适配器可以向TeeReader一样使用,接受一个Reader,并向Writer写入。

发布了7 篇原创文章 · 获赞 1 · 访问量 5890

猜你喜欢

转载自blog.csdn.net/LZX_Not_Afraid/article/details/104632909