Golang 设计模式:装饰器模式

Golang 设计模式:装饰器模式

tk_sky 62 2024-06-10

Golang 设计模式:装饰器模式

理论

在设计模式上,人们已经找了很多方式尝试去替代继承以回避其缺点。继承作为一种对对象添加额外职责和属性的方法,具有一个显而易见的缺点,就是继承会引入额外的静态特征,特别是在子类数量膨胀的情况下。

例如,针对一个视频的处理工作,可以分为转码和编辑两部分,而转码工作可以分为画质增强(enhance)和视频压缩(compress),编辑可以分为加配乐(music)和加字幕(subtitle)。假设这几种工作互不影响,需要实现一种设计描述不同的视频处理操作。按照继承的方法,我们可以创建一种视频处理基类,再创建继承它的画质增强类和视频压缩类,然后再对应创建继承他们的配乐-画质增强类、配乐-视频压缩类、加字幕-画质增强类和加字幕-视频压缩类,这样来通过继承的方式描述一个工作并使其拥有父类的功能。

image-20240609215338641

然而,在现实状态下,我们对功能的需求是多样灵活的,我们可能同时需要加配乐和加字幕,可能不需要转码只需要编辑,也可能要反复做多次视频增强等。这个时候如果还使用继承,我们就需要对所有组合都创建一个子类,最后子类的数量肯定无法收敛。

可以看出,我们不能通过分类-组合-枚举的方法来描述,而是应当把注意力聚焦在功能的添加上:

  • 不再区分转码和编辑,每个处理工作都是独立的
  • 针对每一种处理工作,我们定义一个装饰器类,如视频压缩装饰器
  • 每次使用一个装饰器类时,对应的逻辑就是对原视频进行对应的处理

image-20240609215403897

这样,只要工作的种类确定,都可以轻松组装成不同的工作逻辑。如果后续要添加新的工作种类,我们都只用声明对应的装饰器类即可,而不用去枚举出组合类。

对比:

  • 继承强调等级结构、子类,这种等级结构是需要提前明确的,不方便修改
  • 装饰器模式强调装饰的过程,而不强调输入(等级结构)和输出(子类),能动态地为对象增加某种特定的附属能力,相比继承模式更加灵活,且符合开闭原则(对扩展开放,对修改关闭),不修改基类。

实现

接下来用 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")
}

运行结果:

image-20240609234846393

除了这种实现方式,在小徐先生博客还看到一种闭包实现的装饰器模式:

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 供当前的拦截器使用。

image-20240610001312748

总结

装饰器模式是一种结构型模式,通过对现有类进行包装,能够动态地为对象增加或修改某种特定的功能,同时可以避免通过继承引入大量的静态特征,适合在不增加大量子类的要求下扩展类的功能,或需要动态添加/撤销对象的功能时使用,可作为一种替代继承来扩展对象功能的方式。核心角色有抽象组件接口、具体组件(被修饰的核心对象)、抽象修饰器接口和具体修饰器。