设计模式那些事(4)——常见结构型模式在Go中的应用

上一篇创建型(单例/工厂/建造者)设计模式在Go中的应用介绍了一些常见的创建型设计模式,创建型主要解决了类的创建问题,使代码更易用,而我们经常遇到另外一种问题:类或对象如何组合在一起效率更高,
结构型模式便是解决这类问题的经典结构。

结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面
模式、组合模式、享元模式。

接下来我们就来看一看它们的应用场景:

一、代理模式

原理

代理模式是指在不改变原始类 (或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。

一般情况下,我们让代理类和原始类实现同样的接口,因此你可将其传递给任何一个使用实际服务对象的客户端。

应用场景

代理模式常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只需要关注业务方面的开发。除此之外,代理模式还可以用在 RPC、缓存等应用场景中。

应用

server提供访问服务,代理在它的基础上增加了接口限流的功能

package main

import "fmt"

type server interface {
    handleRequest(string, string) (int, string)
}

type Application struct {
}

func (a *Application) handleRequest(url, method string) (int, string) {
    if url == "/app/status" && method == "GET" {
        return 200, "Ok"
    }

    if url == "/create/user" && method == "POST" {
        return 201, "User Created"
    }
    return 404, "Not Ok"
}

type ApplicationProxy struct {
    application       *Application
    maxAllowedRequest int
    rateLimiter       map[string]int
}

func newApplicationProxy() *ApplicationProxy {
    return &ApplicationProxy{
        application:       &Application{},
        maxAllowedRequest: 2,
        rateLimiter:       make(map[string]int),
    }
}

func (p *ApplicationProxy) handleRequest(url, method string) (int, string) {
    allowed := p.checkRateLimiting(url)
    if !allowed {
        return 403, "Not Allowed"
    }
    return p.application.handleRequest(url, method)
}

func (p *ApplicationProxy) checkRateLimiting(url string) bool {
    if p.rateLimiter[url] == 0 {
        p.rateLimiter[url] = 1
    }
    if p.rateLimiter[url] > p.maxAllowedRequest {
        return false
    }
    p.rateLimiter[url] = p.rateLimiter[url] + 1
    return true
}

func main() {
    server := newApplicationProxy()
    appStatusURL := "/app/status"
    createuserURL := "/create/user"

    httpCode, body := server.handleRequest(appStatusURL, "GET")
    fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

    httpCode, body = server.handleRequest(appStatusURL, "GET")
    fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

    httpCode, body = server.handleRequest(appStatusURL, "GET")
    fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

    httpCode, body = server.handleRequest(createuserURL, "POST")
    fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)

    httpCode, body = server.handleRequest(createuserURL, "GET")
    fmt.Printf("\nUrl: %s\nHttpCode: %d\nBody: %s\n", appStatusURL, httpCode, body)
}

打印结果:

Url: /app/status
HttpCode: 200
Body: Ok

Url: /app/status
HttpCode: 200
Body: Ok

Url: /app/status
HttpCode: 403
Body: Not Allowed

Url: /app/status
HttpCode: 201
Body: User Created

Url: /app/status
HttpCode: 404
Body: Not Ok

二、桥接模式

定义

桥接模式可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而实现解耦和易扩展。

定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中 的“实现”,也并非“接口的实现类”,而是的一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。

使用场景

如果你想要拆分或重组一个具有多重功能的庞杂类(例如能与多个数据库服务器进行交互的类),可以使用桥接模式。

如果你希望在几个独立维度上扩展一个类,可使用该模式。

如果你需要在运行时切换不同实现方法可使用桥接模式。

应用

假设你有两台电脑:一台 Mac 和一台 ThinkPad。
还有两个音响:蓝牙音响和HIFI音响。
这两台电脑和音响可能会任意组合使用,对应四个类:
Mac-蓝牙音响
Mac-蓝牙音响
ThinkPad-HIFI音响
ThinkPad-蓝牙音响

如果引入新的音响,我们也不会希望代码量成倍增长。所以,我们创建了两个层次结构,而不是 2x2 组合的四个结构体:
抽象层: 代表计算机
实施层: 代表音响
这两个层次可通过桥接进行沟通,其中抽象层(计算机)包含对于实施层 (音响)的引用。
抽象层和实施层均可独立开发,不会相互影响。
后面接入新的音响也不需要改动代码,只需要添加新的音响实现类,然后传入计算机类使用就可以了。

package main

import "fmt"

type Computer interface {
    PalyMusic()
}

type Mac struct {
    speaker  Speaker
}

func (m *Mac) PalyMusic() {
    fmt.Println("mac play music")
    m.speaker.Paly()
}

func (m *Mac) SetSpeaker(s Speaker) {
    m.speaker = s
}

type ThinkPad struct {
    speaker  Speaker
}

func (w *ThinkPad) PalyMusic() {
    fmt.Println("ThinkPad play music")
    w.speaker.Paly()
}

func (w *ThinkPad) SetSpeaker(s Speaker) {
    w.speaker = s
}

type Speaker interface {
    Paly()
}

type BluetoothSpeaker struct {
}

func (p *BluetoothSpeaker) Paly() {
    fmt.Println("playing by a  bluetoothSpeaker")
}

type HIFISpeaker struct {
}

func (p *HIFISpeaker) Paly() {
    fmt.Println("playing by a HIFISpeaker")
}

func main(){
    HIFI := &HIFISpeaker{}
    bluetooth := &BluetoothSpeaker{}

    mac := &Mac{}
    thinkPad := &ThinkPad{}

    mac.SetSpeaker(HIFI)
    mac.PalyMusic()
    

    mac.SetSpeaker(bluetooth)
    mac.PalyMusic()


    thinkPad.SetSpeaker(bluetooth)
    thinkPad.PalyMusic()
}

输出

mac play music
playing by a HIFISpeaker

mac play music
playing by a  bluetoothSpeaker

ThinkPad play music
playing by a  bluetoothSpeaker

三、装饰器模式

定义

代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能,两者在代码实现上是差不多的。

应用

装饰模式能够对敏感数据进行加解密,从而将数据从使用数据的代码中独立出来。

package main

import "fmt"

type  DataSource interface{
    writeData()
    readData()
}

type CompanyDataSource struct{

}

func (c *CompanyDataSource) writeData(){
    fmt.Println("写入数据")
}

func (c *CompanyDataSource) readData(){
    fmt.Println("读出数据")
}

type DataSourceDecorator struct{
    dataSource DataSource
}

func NewDataSourceDecorator(dataSource DataSource)*DataSourceDecorator {
    return &DataSourceDecorator{
        dataSource:dataSource,
    }
}

func (d *DataSourceDecorator) writeData(){
    fmt.Println("加密数据")
    d.dataSource.writeData()
}

func (d *DataSourceDecorator) readData(){
    d.dataSource.readData()
    fmt.Println("解密数据")
}

func main(){
    companyDataSource:=&CompanyDataSource{}
    dataSourceDecorator:=NewDataSourceDecorator(companyDataSource)
    dataSourceDecorator.writeData()
    dataSourceDecorator.readData()
}

输出

加密数据
写入数据
读出数据
解密数据

四、适配器模式

定义

适配器模式是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

应用场景

1. 封装有缺陷的接口设计

一般来说,适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。

2. 统一多个类的接口设计

某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。

3. 替换依赖的外部系统

当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。

4. 兼容老版本接口

5、适配不同格式的数据

应用

新款mac只有DP口,电源线可以直接连接,但是要插上USB接口的键盘就必须要使用转换头了,这个转换头就可以理解为我们的适配器。

package main

import "fmt"

//电源
type Power struct {

}

func (p *Power) InsertIntoDPPort(computer Computer){
    fmt.Println("power inserts DP connector into computer.")
    computer.DPPort()
}

//键盘
type Keyboard struct {
}

func (p *Keyboard) InsertIntoUSBPort(computer Computer){
    fmt.Println("keyboard inserts USB connector into computer.")
    computer.USBPort()
}

type Computer interface {
    USBPort()
    DPPort()
}

type Mac struct {
}

func (m *Mac) DPPort(){
    fmt.Println("DP connector is plugged into mac machine.")
}

type  ComputerAdapter struct{
    mac *Mac
}

func (u *ComputerAdapter) USBPort(){
    fmt.Println("Adapter converts USB signal to DP.")
    u.mac.DPPort()
}

func (u *ComputerAdapter) DPPort(){
    u.mac.DPPort()
}

func main(){
    power:=&Power{}
    adapter:=&ComputerAdapter{}
    power.InsertIntoDPPort(adapter)
    
    fmt.Println()
    keyboard:=&Keyboard{}
    keyboard.InsertIntoUSBPort(adapter)
}

输出

power inserts DP connector into computer.
DP connector is plugged into mac machine.

keyboard inserts USB connector into computer.
Adapter converts USB signal to DP.
DP connector is plugged into mac machine.

五、门面模式

定义

门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。

假设有一个系统 A,提供了 a、b、c、d 四个接口。系统 B 完成某个业务功能,需要调用 A 系统的 a、b、d 接口。利用门面模式,我们提供一个包裹 a、b、d 接口调用的门面接口 x,给系统 B 直接使用。

App 客户端调用一次接口 x,来获取到所有想要的数据,将网络通信的次数从 3 次减少到 1 次,也就提高了 App 的响应速度。

应用场景

1. 解决易用性问题

门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。比如,Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。

2. 解决性能问题

我们通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 App 客户端的响应速度。

3. 解决分布式事务问题

在一个金融系统中,有两个业务领域模型,用户和钱包。假设有这样一个业务场景: 在用户注册的时候,我们不仅会创建用户(在数据库 User 表中),还会给用户创建一个钱包(在数据库的 Wallet 表中)。

对于这样一个简单的业务需求,我们可以通过依次调用用户的创建接口和钱包的创建接口来完成。但是,用户注册需要支持事务,最简单的解决方案是,利用数据库事务或者框架提供的事务,执行创建用户和创建钱包这两个 SQL 操作。

实现

package main

import "fmt"

//电视机
type TV struct {}

func (t *TV) On() {
    fmt.Println("打开 电视机")
}

func (t *TV) Off() {
    fmt.Println("关闭 电视机")
}


//电视机
type VoiceBox struct {}

func (v *VoiceBox) On() {
    fmt.Println("打开 音箱")
}

func (v *VoiceBox) Off() {
    fmt.Println("关闭 音箱")
}

//灯光
type Light struct {}

func (l *Light) On() {
    fmt.Println("打开 灯光")
}

func (l *Light) Off() {
    fmt.Println("关闭 灯光")
}


//游戏机
type Xbox struct {}

func (x *Xbox) On() {
    fmt.Println("打开 游戏机")
}

func (x *Xbox) Off() {
    fmt.Println("关闭 游戏机")
}


//麦克风
type MicroPhone struct {}

func (m *MicroPhone) On() {
    fmt.Println("打开 麦克风")
}

func (m *MicroPhone) Off() {
    fmt.Println("关闭 麦克风")
}

//投影仪
type Projector struct {}

func (p *Projector) On() {
    fmt.Println("打开 投影仪")
}

func (p *Projector) Off() {
    fmt.Println("关闭 投影仪")
}


//家庭影院(外观)
type HomePlayerFacade struct {
    tv TV
    vb VoiceBox
    light Light
    xbox Xbox
    mp MicroPhone
    pro Projector
}


//KTV模式
func (hp *HomePlayerFacade) DoKTV() {
    fmt.Println("家庭影院进入KTV模式")
    hp.tv.On()
    hp.pro.On()
    hp.mp.On()
    hp.light.Off()
    hp.vb.On()
}

//游戏模式
func (hp *HomePlayerFacade) DoGame() {
    fmt.Println("家庭影院进入Game模式")
    hp.tv.On()
    hp.light.On()
    hp.xbox.On()
}

func main() {
    homePlayer := new(HomePlayerFacade)

    homePlayer.DoKTV()

    fmt.Println("------------")

    homePlayer.DoGame()
}

输出

家庭影院进入KTV模式
打开 电视机
打开 投影仪
打开 麦克风
关闭 灯光
打开 音箱
------------
家庭影院进入Game模式
打开 电视机
打开 灯光
打开 游戏机

总结

代理模式
代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式
桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

装饰器模式
装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式
适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

门面模式
门面模式是把一个业务逻辑需要调用多个接口的过程进行封装,让复杂逻辑对使用方透明。

下一篇再讲讲行为型的设计模式,手动再见

参考资料:
1、《设计模式之美》
2、https://refactoringguru.cn/de...
3、Easy搞掂Golang设计模式
4、https://lailin.xyz/post/go-de...

你可能感兴趣的:(设计模式那些事(4)——常见结构型模式在Go中的应用)