关于设计模式:常见结构型设计模式在Go中的应用

9次阅读

共计 8156 个字符,预计需要花费 21 分钟才能阅读完成。

上一篇创立型(单例 / 工厂 / 建造者)设计模式在 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- 蓝牙音响

如果引入新的音响,咱们也不会心愿代码量成倍增长。所以,咱们创立了两个层次结构,而不是 2×2 组合的四个构造体:
形象层:代表计算机
施行层:代表音响
这两个档次可通过桥接进行沟通,其中形象层(计算机)蕴含对于施行层(音响)的援用。
形象层和施行层均可独立开发,不会相互影响。
前面接入新的音响也不须要改变代码,只须要增加新的音响实现类,而后传入计算机类应用就能够了。

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…

正文完
 0