0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

GoF给装饰者模式的定义

元闰子的邀请 来源:元闰子的邀请 作者:元闰子的邀请 2022-06-29 10:22 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

上一篇:【Go实现】实践GoF的23种设计模式:原型模式

简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern--Go-Implementation

简介

我们经常会遇到“给现有对象/模块新增功能”的场景,比如 http router 的开发场景下,除了最基础的路由功能之外,我们常常还会加上如日志、鉴权、流控等 middleware。如果你查看框架的源码,就会发现 middleware 功能的实现用的就是装饰者模式(Decorator Pattern)。

GoF给装饰者模式的定义如下:

Decorators provide a flexible alternative to subclassing for extending functionality. Attach additional responsibilities to an object dynamically.

简单来说,装饰者模式通过组合的方式,提供了能够动态地给对象/模块扩展新功能的能力。理论上,只要没有限制,它可以一直把功能叠加下去,具有很高的灵活性。

如果写过 Java,那么一定对 I/O Stream 体系不陌生,它是装饰者模式的经典用法,客户端程序可以动态地为原始的输入输出流添加功能,比如按字符串输入输出,加入缓冲等,使得整个 I/O Stream 体系具有很高的可扩展性和灵活性。

UML 结构

0c30719e-f700-11ec-ba43-dac502259ad0.jpg

场景上下文

在简单的分布式应用系统(示例代码工程)中,我们设计了 Sidecar 边车模块,它的用处主要是为了 1)方便扩展network.Socket的功能,如增加日志、流控等非业务功能;2)让这些附加功能对业务程序隐藏起来,也即业务程序只须关心看到network.Socket接口即可。

0c4d81d0-f700-11ec-ba43-dac502259ad0.jpg

代码实现

Sidecar 的这个功能场景,很适合使用装饰者模式来实现,代码如下:

//demo/network/socket.go
packagenetwork

//关键点1:定义被装饰的抽象接口
//Socket网络通信Socket接口
typeSocketinterface{
//Listen在endpoint指向地址上起监听
Listen(endpointEndpoint)error
//Close关闭监听
Close(endpointEndpoint)
//Send发送网络报文
Send(packet*Packet)error
//Receive接收网络报文
Receive(packet*Packet)
//AddListener增加网络报文监听者
AddListener(listenerSocketListener)
}

//关键点2:提供一个默认的基础实现
typesocketImplstruct{
listenerSocketListener
}

funcDefaultSocket()*socketImpl{
return&socketImpl{}
}

func(s*socketImpl)Listen(endpointEndpoint)error{
returnInstance().Listen(endpoint,s)
}
...//socketImpl的其他Socket实现方法


//demo/sidecar/flowctrl_sidecar.go
packagesidecar

//关键点3:定义装饰器,实现被装饰的接口
//FlowCtrlSidecarHTTP接收端流控功能装饰器,自动拦截Socket接收报文,实现流控功能
typeFlowCtrlSidecarstruct{
//关键点4:装饰器持有被装饰的抽象接口作为成员属性
socketnetwork.Socket
ctx*flowctrl.Context
}

//关键点5:对于需要扩展功能的方法,新增扩展功能
func(f*FlowCtrlSidecar)Receive(packet*network.Packet){
httpReq,ok:=packet.Payload().(*http.Request)
//如果不是HTTP请求,则不做流控处理
if!ok{
f.socket.Receive(packet)
return
}
//流控后返回429TooManyRequest响应
if!f.ctx.TryAccept(){
httpResp:=http.ResponseOfId(httpReq.ReqId()).
AddStatusCode(http.StatusTooManyRequest).
AddProblemDetails("enterflowctrlstate")
f.socket.Send(network.NewPacket(packet.Dest(),packet.Src(),httpResp))
return
}
f.socket.Receive(packet)
}

//关键点6:不需要扩展功能的方法,直接调用被装饰接口的原生方法即可
func(f*FlowCtrlSidecar)Close(endpointnetwork.Endpoint){
f.socket.Close(endpoint)
}
...//FlowCtrlSidecar的其他方法

//关键点7:定义装饰器的工厂方法,入参为被装饰接口
funcNewFlowCtrlSidecar(socketnetwork.Socket)*FlowCtrlSidecar{
return&FlowCtrlSidecar{
socket:socket,
ctx:flowctrl.NewContext(),
}
}

//demo/sidecar/all_in_one_sidecar_factory.go
//关键点8:使用时,通过装饰器的工厂方法,把所有装饰器和被装饰者串联起来
func(aAllInOneFactory)Create()network.Socket{
returnNewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()),a.producer)
}

总结实现装饰者模式的几个关键点:

  1. 定义需要被装饰的抽象接口,后续的装饰器都是基于该接口进行扩展。
  2. 为抽象接口提供一个基础实现。
  3. 定义装饰器,并实现被装饰的抽象接口。
  4. 装饰器持有被装饰的抽象接口作为成员属性。“装饰”的意思是在原有功能的基础上扩展新功能,因此必须持有原有功能的抽象接口。
  5. 在装饰器中,对于需要扩展功能的方法,新增扩展功能。
  6. 不需要扩展功能的方法,直接调用被装饰接口的原生方法即可
  7. 为装饰器定义一个工厂方法,入参为被装饰接口。
  8. 使用时,通过装饰器的工厂方法,把所有装饰器和被装饰者串联起来。

扩展

Go 风格的实现

在 Sidecar 的场景上下文中,被装饰的Socket是一个相对复杂的接口,装饰器通过实现Socket接口来进行功能扩展,是典型的面向对象风格。

如果被装饰者是一个简单的接口/方法/函数,我们可以用更具 Go 风格的实现方式,考虑前文提到的 http router 场景。如果你使用原生的net/http进行 http router 开发,通常会这么实现:

funcmain(){
//注册/hello的router
http.HandleFunc("/hello",hello)
//启动http服务器
http.ListenAndServe("localhost:8080",nil)
}

//具体的请求处理逻辑,类型是http.HandlerFunc
funchello(whttp.ResponseWriter,r*http.Request){
w.Write([]byte("hello,world"))
}

其中,我们通过http.HandleFunc来注册具体的 router,hello是具体的请求处理方法。现在,我们想为该 http 服务器增加日志、鉴权等通用功能,那么可以把func(w http.ResponseWriter, r *http.Request)作为被装饰的抽象接口,通过新增日志、鉴权等装饰器完成功能扩展。

//demo/network/http/http_handle_func_decorator.go

//关键点1:确定被装饰接口,这里为原生的http.HandlerFunc
typeHandlerFuncfunc(ResponseWriter,*Request)

//关键点2:定义装饰器类型,是一个函数类型,入参和返回值都是http.HandlerFunc函数
typeHttpHandlerFuncDecoratorfunc(http.HandlerFunc)http.HandlerFunc

//关键点3:定义装饰函数,入参为被装饰的接口和装饰器可变列表
funcDecorate(hhttp.HandlerFunc,decorators...HttpHandlerFuncDecorator)http.HandlerFunc{
//关键点4:通过for循环遍历装饰器,完成对被装饰接口的装饰
for_,decorator:=rangedecorators{
h=decorator(h)
}
returnh
}

//关键点5:实现具体的装饰器
funcWithBasicAuth(hhttp.HandlerFunc)http.HandlerFunc{
returnfunc(whttp.ResponseWriter,r*http.Request){
cookie,err:=r.Cookie("Auth")
iferr!=nil||cookie.Value!="Pass"{
w.WriteHeader(http.StatusForbidden)
return
}
//关键点6:完成功能扩展之后,调用被装饰的方法,才能将所有装饰器和被装饰者串起来
h(w,r)
}
}

funcWithLogger(hhttp.HandlerFunc)http.HandlerFunc{
returnfunc(whttp.ResponseWriter,r*http.Request){
log.Println(r.Form)
log.Printf("path%s",r.URL.Path)
h(w,r)
}
}

funchello(whttp.ResponseWriter,r*http.Request){
w.Write([]byte("hello,world"))
}

funcmain(){
//关键点7:通过Decorate函数完成对hello的装饰
http.HandleFunc("/hello",Decorate(hello,WithLogger,WithBasicAuth))
//启动http服务器
http.ListenAndServe("localhost:8080",nil)
}

上述的装饰者模式的实现,用到了类似于Functional Options的技巧,也是巧妙利用了 Go 的函数式编程的特点,总结下来有如下几个关键点:

  1. 确定被装饰的接口,上述例子为http.HandlerFunc
  2. 定义装饰器类型,是一个函数类型,入参和返回值都是被装饰接口,上述例子为func(http.HandlerFunc) http.HandlerFunc
  3. 定义装饰函数,入参为被装饰的接口和装饰器可变列表,上述例子为Decorate方法。
  4. 在装饰方法中,通过for循环遍历装饰器,完成对被装饰接口的装饰。这里是用来类似Functional Options的技巧,一定要注意装饰器的顺序
  5. 实现具体的装饰器,上述例子为WithBasicAuthWithLogger函数。
  6. 在装饰器中,完成功能扩展之后,记得调用被装饰者的接口,这样才能将所有装饰器和被装饰者串起来。
  7. 在使用时,通过装饰函数完成对被装饰者的装饰,上述例子为Decorate(hello, WithLogger, WithBasicAuth)

Go 标准库中的装饰者模式

在 Go 标准库中,也有一个运用了装饰者模式的模块,就是context,其中关键的接口如下:

packagecontext

//被装饰接口
typeContextinterface{
Deadline()(deadlinetime.Time,okbool)
Done()<-chanstruct{}
Err()error
Value(keyany)any
}

//cancel装饰器
typecancelCtxstruct{
Context//被装饰接口
musync.Mutex
doneatomic.Value
childrenmap[canceler]struct{}=
errerror
}
//cancel装饰器的工厂方法
funcWithCancel(parentContext)(ctxContext,cancelCancelFunc){
//...
c:=newCancelCtx(parent)
propagateCancel(parent,&c)
return&c,func(){c.cancel(true,Canceled)}
}

//timer装饰器
typetimerCtxstruct{
cancelCtx//被装饰接口
timer*time.Timer

deadlinetime.Time
}
//timer装饰器的工厂方法
funcWithDeadline(parentContext,dtime.Time)(Context,CancelFunc){
//...
c:=&timerCtx{
cancelCtx:newCancelCtx(parent),
deadline:d,
}
//...
returnc,func(){c.cancel(true,Canceled)}
}
//timer装饰器的工厂方法
funcWithTimeout(parentContext,timeouttime.Duration)(Context,CancelFunc){
returnWithDeadline(parent,time.Now().Add(timeout))
}

//value装饰器
typevalueCtxstruct{
Context//被装饰接口
key,valany
}
//value装饰器的工厂方法
funcWithValue(parentContext,key,valany)Context{
ifparent==nil{
panic("cannotcreatecontextfromnilparent")
}
//...
return&valueCtx{parent,key,val}
}
0c708ad6-f700-11ec-ba43-dac502259ad0.jpg

使用时,可以这样:

//使用时,可以这样
funcmain(){
ctx:=context.Background()
ctx=context.WithValue(ctx,"key1","value1")
ctx,_=context.WithTimeout(ctx,time.Duration(1))
ctx=context.WithValue(ctx,"key2","value2")
}

不管是 UML 结构,还是使用方法,context模块都与传统的装饰者模式有一定出入,但也不妨碍context是装饰者模式的典型运用。还是那句话,学习设计模式,不能只记住它的结构,而是学习其中的动机和原理

典型使用场景

  • I/O 流,比如为原始的 I/O 流增加缓冲、压缩等功能。
  • Http Router,比如为基础的 Http Router 能力增加日志、鉴权、Cookie等功能。
  • ......

优缺点

优点

  1. 遵循开闭原则,能够在不修改老代码的情况下扩展新功能。
  2. 可以用多个装饰器把多个功能组合起来,理论上可以无限组合。

缺点

  1. 一定要注意装饰器装饰的顺序,否则容易出现不在预期内的行为。
  2. 当装饰器越来越多之后,系统也会变得复杂。

与其他模式的关联

装饰者模式和代理模式具有很高的相似性,但是两种所强调的点不一样。前者强调的是为本体对象添加新的功能;后者强调的是对本体对象的访问控制

装饰者模式和适配器模式的区别是,前者只会扩展功能而不会修改接口;后者则会修改接口。

文章配图

可以在用Keynote画出手绘风格的配图中找到文章的绘图方法。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 模块
    +关注

    关注

    7

    文章

    2848

    浏览量

    53429
  • UML
    UML
    +关注

    关注

    0

    文章

    123

    浏览量

    31658

原文标题:【Go实现】实践GoF的23种设计模式:装饰者模式

文章出处:【微信号:yuanrunzi,微信公众号:元闰子的邀请】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    VirtualLab:Ince高斯模式

    **摘要 ** 除了Hermite和Laguerre高斯模式之外,近轴波动方程还有第三种严格的正交解族,即所谓的Ince高斯模式。这些解在椭圆坐标中定义,并且通过椭圆参数允许在Hermite
    发表于 03-20 08:58

    VirtualLab:Ince高斯模式

    **摘要 ** 除了Hermite和Laguerre高斯模式之外,近轴波动方程还有第三种严格的正交解族,即所谓的Ince高斯模式。这些解在椭圆坐标中定义,并且通过椭圆参数允许在Hermite
    发表于 03-19 08:36

    KiCad 10 IPC API 开发问答整理

    : 兼容性保留:  依赖旧版 pcbnew.py (SWIG) 的插件在 KiCad 10 中依然可以使用,了开发更多的缓冲时间。 功能缺席:   无头模式 (Headless Mode
    的头像 发表于 01-07 11:20 898次阅读

    嵌入式程序设计中4种常用模式

    。 如果不具备此条件,则必须作为框架的重要约定,禁止二次开发产生此类问题。 4. 装饰模式 装饰模式赋予了框架在后期增加功能的能力。框
    发表于 12-25 07:12

    无图形界面模式下自定义检查工具的应用

    此前文章已介绍 ANSA 中的自定义检查工具。本文将探讨该功能在无图形界面(No-GUI)模式下的应用,旨在满足标准化工作流程的需求,适用于需要高度自动化的前处理场景。通过集成自定义检查,用户可实现工作流程的高效自动化运行。
    的头像 发表于 11-30 14:13 751次阅读
    无图形界面<b class='flag-5'>模式</b>下自<b class='flag-5'>定义</b>检查工具的应用

    浮点扩展指令集中定义的五种舍入模式

    本文主要描述浮点扩展指令集中定义的五种舍入模式,并介绍一些实现时要注意的地方。 舍入模式介绍 首先,在riscv-spec-v2.2的浮点指令集扩展部分一共定义了五种不同的舍入
    发表于 10-24 10:25

    用LabVIEW开发的测试软件,支持自定义测试内容,分享大家。

    用LabVIEW开发的测试软件,支持自定义测试内容,分享大家。链接自取 链接: https://pan.baidu.com/s/14KtGsFmeFJ9ZkeVPygz2YQ?pwd=v8q7 提取码: v8q7
    发表于 10-22 10:35

    一文读懂 RGB接口的 DE模式 和 行场(HV)模式 区别

    在了解RGB接口DE模式和行场(HV)模式 模式前,我们先看一下RGB接口屏幕的引脚定义: 下图来之一款非常主流的工业级TFT的手册引脚定义
    发表于 09-18 14:18

    关于生命周期中的aboutToAppear和onPageShow的理解和应用

    过程、应用进入前台等场景,仅@Entry装饰的自定义组件作为页面时生效。 从两相同的角度来说,其都是在自定义组件显示后,主动去触发的生命周期,在这两个生命周期里可以写一些数据获取啊等
    发表于 06-30 17:32

    AG32 SDK:加入DSP例程及支持boot_mode模式和自定义 Linker脚本等(v1.7.5版本)

    数据从 Flash 加载到 SRAM。这种方式虽然提升了运行时性能,但也带来了更高的内存占用。 引入的 flash_rodata 模式允许开发选择将常量数据始终保留在 Flash 中,不再复制到
    发表于 05-20 14:14

    基于蓝牙模组Beacon+观察模式实现资产管理和室内定位

    的一种广播协议设备(从机)。Beacon主要参数①uuid②major③minor④companyID观察模式1.用于监听其他设备的广播数据而不与之建立连接;2.
    的头像 发表于 05-15 19:34 1005次阅读
    基于蓝牙模组Beacon+观察<b class='flag-5'>者</b><b class='flag-5'>模式</b>实现资产管理和室内定位

    PCBA代工代料:定义、类型与精准选型指南

    时又该如何做出明智决策?本文将为您深入剖析。 一、PCBA代工代料:定义与内涵 PCBA代工代料,顾名思义,是指企业将PCBA产品的制造任务,包括电路板(PCB)与电子元器件的采购、组装等环节,委托专业的代工厂商完成。这一模式
    的头像 发表于 05-09 10:08 1456次阅读

    VirtualLab Fusion应用:Ince-Gaussian模式

    摘要 除了厄米和拉盖尔高斯光束模式外,波动方程在傍轴情况还有第三种严格的正交解系——即所谓的Ince-Gaussian光束。这些解在椭圆坐标系中定义,并且允许通过椭圆参数实现厄米和拉盖尔高斯光束模式
    发表于 04-30 08:46

    如何在KaihongOS操作系统上写一个弹窗组件

    写一个弹窗组件 KaihongOS框架提供了弹窗的API接口,开发可直接使用,详情请参考@ohos.promptAction (弹窗)。但在开发过程中当提供的弹窗接口无法满足需求时,则需要自定义
    发表于 04-30 06:44

    晶科能源4.3MW光伏发电项目落地绍兴正大装饰

    晶科能源携手绍兴博辰智源电力开发有限公司,为绍兴正大装饰城成功落地4.3MW光伏发电项目。这不仅是一项绿色能源的实践,更是传统市场迈向低碳未来的重要一步。
    的头像 发表于 04-28 10:38 916次阅读