第11节 结构型模式(适配器模式)


❤️💕💕Java和Golang的设计模式,设计模式介绍、创建者模式、结构型模式、行为型模式。Myblog:http://nsddd.topopen in new window


[TOC]

结构型模式

模式名称模式名称作用
结构型模式 Structural Pattern (7)适配器模式 ★★★★☆将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
桥接模式 ★★★☆☆将抽象部分与实际部分分离,使它们都可以独立的变化。
组合模式 ★★☆☆☆将对象组合成树形结构以表示“部分--整体”的层次结构。使得用户对单个对象和组合对象的使用具有一致性。
装饰模式 ★★★☆☆动态的给一个对象添加一些额外的职责。就增加功能来说,此模式比生成子类更为灵活。
外观模式 ★★★★★为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
享元模式 ★☆☆☆☆以共享的方式高效的支持大量的细粒度的对象。
代理模式 ★★★★☆为其他对象提供一种代理以控制对这个对象的访问。

什么是适配器

将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

+----------------------+        +--------------------------+  
|   Target Interface   |  <---- |      Client Struct        | 
+----------------------+        +--------------------------+
|                      |        |   - adapter AdapterIface   |
|  TargetMethod() void |        +--------------------------+
|                      |                  |                     
+----------------------+     +-------------------------------+
                               |       Adapter Struct         |
                               +-------------------------------+
                               | - adaptee AdapteeStructIface  |
                               +-------------------------------+
                               | - AdapterMethod() void        |
                               +-------------------------------+

  • 目标接口(Target Interface): 这是客户端(Client)代码期望使用的接口。它定义了一个或多个方法,客户端(Client)通过这些方法来与系统进行交互。
  • 客户端(Client Struct): Clients是使用目标接口(Target Interface)的结构体。他们并不知道实际使用的具体实现,而是通过目标接口(Target Interface)与系统进行交互。客户端(Client)拥有一个指向适配器(Adapter)的引用,该适配器实现了目标接口(Target Interface),并将请求委派给被适配者(Adaptee)。
  • 适配器(Adapter): 适配器(Adapter)是连接客户端(Client)和被适配者(Adaptee)的桥梁。它实现了目标接口(Target Interface),并将客户端(Client)请求转换为被适配者(Adaptee)可以理解的形式。适配器(Adapter)通常包装了一个被适配者(Adaptee)对象,并在目标接口(Target Interface)中定义了一个或多个方法来调用适配器(Adapter)的方法。
  • 被适配者(Adaptee): 被适配者(Adaptee)是系统的一部分,它是适配器(Adapter)所要调用的对象。被适配者(Adaptee)有自己独特的接口,但客户端(Client)不知道如何与其进行交互。

image-20230514182249730

演示

// 目标接口(Target Interface)
type TargetInterface interface {
    Request() string
}

// 被适配者(Adaptee)
type Adaptee struct{}

func (a *Adaptee) SpecificRequest() string {
    return "Specific Request"
}

// 适配器(Adapter)
type Adapter struct {
    adaptee *Adaptee
}

func (ad *Adapter) Request() string {
    return ad.adaptee.SpecificRequest()
}

// 客户端(Client)
type Client struct{}

func (c *Client) ExecuteRequest(target TargetInterface) string {
    return target.Request()
}

func main() {
    adaptee := &Adaptee{}
    adapter := &Adapter{adaptee: adaptee}
    client := &Client{}

    result := client.ExecuteRequest(adapter)
    fmt.Println(result)
}

在上面的例子中,我们使用了一个目标接口(Target Interface),该接口定义了一个Request()方法。然后我们创建了一个被适配者(Adaptee),它实现了不兼容的SpecificRequest() 方法。

接着,我们创建了一个适配器(Adapter),它实现了目标接口(Target Interface),并将适配请求委托给被适配者(Adaptee)的SpecificRequest()方法。

最后,我们创建了一个客户端(Client),它可以执行任何符合目标接口(Target Interface)的请求。在这个例子中,我们将适配器(Adapter)传递给客户端(Client),并调用ExecuteRequest()方法。ExecuteRequest()方法向适配器(Adapter)发出请求,该适配器(Adapter)将请求转换为被适配者(Adaptee)可以理解的形式并返回结果。

这个例子演示了如何使用Go语言实现适配器模式来解决不兼容的接口问题。

场景

一般在调用第三方接口的时候会选择使用适配器模式

比如说 Kubernetes 源码中对 docker 或者 containerd 的时候,选择使用适配器包装,包装为 Kubernetes 中的规范。

虽然现在在 Kubernetes 的新版本源码中被废弃了,但是在之前很长一段时间依旧是选择的适配器。

模拟相关的代码实现:

package main

import "fmt"

// Target Interface, 需要适配成的接口
type ContainerRuntime interface {
    RunContainer(image string) error
}

// 被适配者(Adaptee), docker
type Docker struct{}

func (d *Docker) StartContainer(image string) error {
    fmt.Printf("Starting container with image %s using Docker...\n", image)
    return nil
}

// 适配器(Adapter), 将docker转换为ContainerRuntime接口
type DockerAdapter struct {
    docker *Docker
}

func (da *DockerAdapter) RunContainer(image string) error {
    return da.docker.StartContainer(image)
}

// 被适配者(Adaptee), containerd
type Containerd struct{}

func (c *Containerd) CreateContainer(image string) error {
    fmt.Printf("Creating container with image %s using Containerd...\n", image)
    return nil
}

// 客户端(Client), Kubernetes
type Kubernetes struct{}

func (k *Kubernetes) HandleContainer(runtime ContainerRuntime, image string) {
    runtime.RunContainer(image)
}

func main() {
    docker := &Docker{}
    dockerAdapter := &DockerAdapter{docker: docker}
    containerd := &Containerd{}
    kubernetes := &Kubernetes{}

    // Kubernetes 使用 docker
    kubernetes.HandleContainer(dockerAdapter, "nginx")

    // Kubernetes 使用 containerd,需要创建一个新的Adapter将containerd适配成ContainerRuntime接口
    containerdAdapter := &struct {
        *Containerd
    }{}
    containerdAdapter.Containerd = containerd

    kubernetes.HandleContainer(containerdAdapter, "nginx")
}

在本示例中,我们有两个具体的被适配者(Adaptee):Docker和Containerd。它们都实现了不同的接口,但我们想要将它们适配为统一的ContainerRuntime接口。

为此,我们创建了一个目标接口(ContainerRuntime),其中只定义了一个RunContainer()方法。我们还创建了两个被适配者(Adaptee):Docker和Containerd,它们分别实现了不同的方法来启动容器。

然后,我们创建了一个适配器(Adapter),将Docker适配成ContainerRuntime接口。DockerAdapter结构体包装了一个指向Docker对象的引用,并实现了ContainerRuntime接口的RunContainer()方法。

最后,我们创建了一个客户端(Client),即Kubernetes。Kubernetes结构体包含一个HandleContainer()方法,该方法接受一个ContainerRuntime类型参数和一个镜像名称(image),并使用给定的运行时(runtime)来启动容器。

在本例中,我们使用DockerAdapter将Docker适配成ContainerRuntime接口,并将其传递给Kubernetes。然后,我们创建了一个新的containerdAdapter,将Containerd适配成ContainerRuntime接口,并将其传递给Kubernetes。这样,我们就可以使用不同的容器运行时来启动容器,并且Kubernetes不需要知道底层容器运行时的细节。

因此,本示例演示了如何使用适配器模式来解决Kubernetes与不同容器运行时之间的兼容性问题。

优缺点

优点

(1) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。

(2) 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。

(3) 灵活性和扩展性都非常好,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

缺点:

适配器中置换适配者类的某些方法比较麻烦。

END 链接