Golang 设计模式:装饰器模式
理论
在设计模式上,人们已经找了很多方式尝试去替代继承以回避其缺点。继承作为一种对对象添加额外职责和属性的方法,具有一个显而易见的缺点,就是继承会引入额外的静态特征,特别是在子类数量膨胀的情况下。
例如,针对一个视频的处理工作,可以分为转码和编辑两部分,而转码工作可以分为画质增强(enhance)和视频压缩(compress),编辑可以分为加配乐(music)和加字幕(subtitle)。假设这几种工作互不影响,需要实现一种设计描述不同的视频处理操作。按照继承的方法,我们可以创建一种视频处理基类,再创建继承它的画质增强类和视频压缩类,然后再对应创建继承他们的配乐-画质增强类、配乐-视频压缩类、加字幕-画质增强类和加字幕-视频压缩类,这样来通过继承的方式描述一个工作并使其拥有父类的功能。
然而,在现实状态下,我们对功能的需求是多样灵活的,我们可能同时需要加配乐和加字幕,可能不需要转码只需要编辑,也可能要反复做多次视频增强等。这个时候如果还使用继承,我们就需要对所有组合都创建一个子类,最后子类的数量肯定无法收敛。
可以看出,我们不能通过分类-组合-枚举的方法来描述,而是应当把注意力聚焦在功能的添加上:
- 不再区分转码和编辑,每个处理工作都是独立的
- 针对每一种处理工作,我们定义一个装饰器类,如视频压缩装饰器
- 每次使用一个装饰器类时,对应的逻辑就是对原视频进行对应的处理
这样,只要工作的种类确定,都可以轻松组装成不同的工作逻辑。如果后续要添加新的工作种类,我们都只用声明对应的装饰器类即可,而不用去枚举出组合类。
对比:
- 继承强调等级结构、子类,这种等级结构是需要提前明确的,不方便修改
- 装饰器模式强调
装饰
的过程,而不强调输入
(等级结构)和输出
(子类),能动态地为对象增加某种特定的附属能力,相比继承模式更加灵活,且符合开闭原则(对扩展开放,对修改关闭),不修改基类。
实现
接下来用 golang 实现这个例子对应的设计模式。
首先声明核心类,能处理视频的视频处理器,我们用一个 interface:
type VideoProcessor interface {
// Process 进行工作
Process(videoFile string)
}
VideoProcessor 可以同时定义多种视频处理器的实现类,我们定义其中一个为工作流 Workflow,作为我们需要修饰的类:
type Workflow struct {
}
func NewWorkflow() VideoProcessor {
return &Workflow{}
}
func (*Workflow) Process(videoFile string) {
println("processing video " + videoFile)
}
接下来声明装饰器,定义一个装饰器接口,其本身是强依附于核心类(VideoProcessor)产生的,只是对其进行修饰,因此构造器要传入 VideoProcessor:
type Decorator VideoProcessor
func NewDecorator(p VideoProcessor) Decorator {
return p
}
之后就可以声明装饰器的具体实现类了,每个装饰器类的作用就是对 Processor 进行一轮装饰增强,通过重写 Process 方法,来实现对应的增强装饰效果:
type EnhanceDecorator struct {
Decorator
}
func NewEnhanceDecorator(d Decorator) Decorator {
return &EnhanceDecorator{d}
}
func (e *EnhanceDecorator) Process(videoFile string) {
println("enhancing video " + videoFile)
e.Decorator.Process(videoFile)
}
type CompressDecorator struct {
Decorator
}
func NewCompressDecorator(d Decorator) Decorator {
return &CompressDecorator{d}
}
func (c *CompressDecorator) Process(videoFile string) {
println("compressing video " + videoFile)
c.Decorator.Process(videoFile)
}
接下来就可以走一遍装饰器流程,创建一个装饰过的 VideoProcessor:
func main() {
processor := NewWorkflow()
// 增加增强功能
processor = NewEnhanceDecorator(processor)
// 增加压缩功能
processor = NewCompressDecorator(processor)
// 再压缩一次
processor = NewCompressDecorator(processor)
// 处理视频
processor.Process("youyou.avi")
}
运行结果:
除了这种实现方式,在小徐先生博客还看到一种闭包实现的装饰器模式:
type handleFunc func(ctx context.Context, param map[string]interface{}) error
func Decorate(fn handleFunc) handleFunc {
return func(ctx context.Context, param map[string]interface{}) error {
// preprocess..
err := fn(ctx, param)
// postprocess..
return err
}
}
这种方法用 handleFunc 作为核心,Decorate 方法作为装饰器,每次执行 Decorate 方法时都会在handleFunc 前后增加一些额外的附属逻辑。
应用
小徐先生博客给到了一个比较好的应用示例:grpc-go 的拦截器 Interceptor
。这个东西在之前做 cubeCache 的 grpc 反向代理时有关注到。当 grpc 服务器收到来自客户端的 grpc 请求时,在用户注册的 handler 得到执行之前,会先触发拦截器链 chainUnaryInterceptors
的遍历调用,一步步触发注册的拦截器。
这个例子中,handler 可以理解为装饰器模式中的核心类,拦截器链中的每个拦截器 unaryInterceptor 可以理解为一个装饰器。
对于装饰器
拦截器,定义是
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
其中 UnaryHandler 定义为
type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)
在准备拦截器链时,会组装一个 UnaryServerInterceptor 类型的函数:
func chainUnaryInterceptors(interceptors []UnaryServerInterceptor) UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (interface{}, error) {
return interceptors[0](ctx, req, info, getChainUnaryHandler(interceptors, 0, info, handler))
}
}
func getChainUnaryHandler(interceptors []UnaryServerInterceptor, curr int, info *UnaryServerInfo, finalHandler UnaryHandler) UnaryHandler {
if curr == len(interceptors)-1 {
return finalHandler
}
return func(ctx context.Context, req interface{}) (interface{}, error) {
return interceptors[curr+1](ctx, req, info, getChainUnaryHandler(interceptors, curr+1, info, finalHandler))
}
}
它的逻辑是,从头依次使用下一个拦截器对核心方法 handler 进行装饰包裹,封装成一个新的 handler 供当前的拦截器使用。
总结
装饰器模式是一种结构型模式,通过对现有类进行包装,能够动态地为对象增加或修改某种特定的功能,同时可以避免通过继承引入大量的静态特征,适合在不增加大量子类的要求下扩展类的功能,或需要动态添加/撤销对象的功能时使用,可作为一种替代继承来扩展对象功能的方式。核心角色有抽象组件接口、具体组件(被修饰的核心对象)、抽象修饰器接口和具体修饰器。