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

    文章

    2655

    浏览量

    47290
  • UML
    UML
    +关注

    关注

    0

    文章

    122

    浏览量

    30839

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

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

收藏 人收藏

    评论

    相关推荐

    【每天学点AI】一个例子带你了解Python装饰器到底在干嘛!

    今天我们来聊聊一种能给你的代码变得“加料”的神器——Python装饰器。就像一杯咖啡,原本它是苦的,为了让它符合我的口味,我给它添加了糖,添加之后就完美的符合了我的口味。那么,装饰器又是如何代码
    的头像 发表于 09-20 16:54 511次阅读
    【每天学点AI】一个例子带你了解Python<b class='flag-5'>装饰</b>器到底在干嘛!

    HarmonyOS实战开发-深度探索与打造个性化自定义组件

    ,容器组件,媒体组件,绘制组件,画布组件组件等,如Button、Text 是基础组件。 由开发在基础组件基础上 添加一些封装和修饰 定义的组件称为自定义组件。自定义组件的实现大大提高
    发表于 05-08 16:30

    实践GoF的23种设计模式实现:桥接模式

    也即,将抽象部分和实现部分进行解耦,使得它们能够各自往独立的方向变化。
    的头像 发表于 04-14 09:30 394次阅读
    实践<b class='flag-5'>GoF</b>的23种设计<b class='flag-5'>模式</b>实现:桥接<b class='flag-5'>模式</b>

    鸿蒙OS开发实例:【ArkTS类库多线程@Concurrent装饰器校验并发函数】

    在使用TaskPool时,执行的并发函数需要使用该装饰器修饰,否则无法通过相关校验。从API version 9开始,该装饰器支持在ArkTS卡片中使用。
    的头像 发表于 04-02 14:45 562次阅读
    鸿蒙OS开发实例:【ArkTS类库多线程@Concurrent<b class='flag-5'>装饰</b>器校验并发函数】

    实践GoF的23种设计模式:解释器模式

    解释器模式(Interpreter Pattern)应该是 GoF 的 23 种设计模式中使用频率最少的一种了,它的应用场景较为局限。
    的头像 发表于 04-01 11:01 600次阅读
    实践<b class='flag-5'>GoF</b>的23种设计<b class='flag-5'>模式</b>:解释器<b class='flag-5'>模式</b>

    LoRa模组FSK模式下WOR功耗估算实例

    的LoRa模式,而是使用的该模组的FSK模式,并需要使用FSK模式下的WOR功能。在确定使用FSK模式下的WOR功能时需要先估算功耗是否能满足使用
    的头像 发表于 03-29 08:12 4307次阅读
    LoRa模组FSK<b class='flag-5'>模式</b>下WOR功耗估算实例

    鸿蒙OS开发实例:【装饰器@Observed@ObjectLink】

    加深对@Observed@ObjectLink 装饰器使用的理解,以小故事做注释
    的头像 发表于 03-28 17:05 920次阅读
    鸿蒙OS开发实例:【<b class='flag-5'>装饰</b>器@Observed@ObjectLink】

    鸿蒙原生应用开发-ArkTS语言基础类库多线程@Concurrent装饰器校验并发函数

    在使用TaskPool时,执行的并发函数需要使用该装饰器修饰,否则无法通过相关校验。从API version 9开始,该装饰器支持在ArkTS卡片中使用。 装饰器说明 装饰器使用示例
    发表于 03-18 10:30

    OpenHarmony父子组件单项同步使用:@Prop装饰

    @Prop装饰的变量可以和父组件建立单向的同步关系。@Prop装饰的变量是可变的,但是变化不会同步回其父组件。 说明: 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 概述
    的头像 发表于 02-03 10:57 373次阅读
    OpenHarmony父子组件单项同步使用:@Prop<b class='flag-5'>装饰</b>器

    OpenHarmony 定义扩展组件样式:@Extend 装饰

    说明: 从 API version 9 开始,该装饰器支持在 ArkTS 卡片中使用。 装饰器使用说明 语法   @Extend(UIComponentName) function
    的头像 发表于 02-01 20:53 248次阅读

    什么是观察设计模式?Golang中的观察模式介绍

    当涉及到订单处理系统时,观察设计模式可以用于实现订单状态的变化和通知。
    的头像 发表于 01-08 10:08 390次阅读

    ADE7880除了线周期累计模式还有其他的累计模式吗?两的区别是什么?

    您好: 有几个问题要请教下: 1、在ADE7880中在关于电能累计这一章节中说到了线周期功率累计模式,这是一种怎样的模式? 2、除了线周期累计模式还有其他的累计模式吗?两
    发表于 12-27 08:03

    如何C语言中的函数定义两个不同的名字?

    最近有位哥问我,如何C语言中的函数定义两个不同的名字?就是这两个名字都是指向同一个函数,同一个地址,而且两个名字都可以当做函数来用的那种。
    的头像 发表于 12-19 16:21 709次阅读

    实践GoF的23种设计模式:适配器模式

    适配器模式所做的就是将一个接口 Adaptee,通过适配器 Adapter 转换成 Client 所期望的另一个接口 Target 来使用,实现原理也很简单,就是 Adapter 通过实现 Target接口,并在对应的方法中调用 Adaptee 的接口实现。
    的头像 发表于 12-10 14:00 460次阅读
    实践<b class='flag-5'>GoF</b>的23种设计<b class='flag-5'>模式</b>:适配器<b class='flag-5'>模式</b>

    实践GoF的23种设计模式:备忘录模式

    相对于代理模式、工厂模式等设计模式,备忘录模式(Memento)在我们日常开发中出镜率并不高,除了应用场景的限制之外,另一个原因,可能是备忘录模式
    的头像 发表于 11-25 09:05 503次阅读
    实践<b class='flag-5'>GoF</b>的23种设计<b class='flag-5'>模式</b>:备忘录<b class='flag-5'>模式</b>