Golang 设计模式之装饰器模式

文摘   科技   2023-06-22 16:31   北京  

1 装饰器模式

本期和大家交流的是设计模式中的装饰器模式.

下面聊聊关于装饰器模式的基本定义:装饰器模式能够在不改变原对象结构的基础上,动态地为对象增加附属能力. 在实现思路上,装饰器模式和“继承”一定的类似之处,但是两者侧重点有所不同,可以把装饰器模式作为“继承”的一种补充手段.

这么干讲概念显得过于抽象,下面我们通过一个实际案例,来和大家具体地剖析一下有关于装饰器模式的实现思路:

  • • 现在摆在我们面前的时一碗热腾腾的白米饭,我们需要在此基础上添加出各种配菜组合,搭配出一款美味的盖浇饭.

  • • 当前可供我们选择主菜是几种肉食,包括培根、牛肉还有鸡排

  • • 除了主菜外,还有几道副菜作为调剂,包括鸡蛋、青椒还有黑椒汁

主食主菜副菜
米饭培根鸡蛋
米饭牛肉青椒
米饭鸡排黑椒汁

 

基于以上条件,我们开始烹饪创作:

  • • 比如选用主菜培根搭配上副菜鸡蛋,就形成了一碗鸡蛋培根盖浇饭;

  • • 比如选用主菜牛肉搭配上副菜青椒,就形成了一碗青椒牛肉盖浇饭;

  • • 比如选用主菜鸡排搭配上副菜黑椒汁,就形成了一碗黑椒鸡排盖浇饭.

聊到这里,下面我们尝试通过编程的方式还原上面的场景问题.

一种常见的实现方式是可以采用继承的方式进行实现:

  • • 我们构造出一个最基础的父类:米饭

  • • 在白米饭的基础上,根据添加的主菜肉食,实现出现对应的几个一级子类:培根饭、牛肉饭、鸡排饭

  • • 在一级子类的基础上,再搭配上各种副菜,实现对应的几个二级子类,包括:鸡蛋培根饭、青椒培根饭、青椒牛肉饭、黑椒鸡排饭等等

在上述“继承”的实现思路中,我们需要对子类的等级以及种类进行枚举,包括通过加入主菜后形成的一系列一级子类以及加入主菜和副菜后形成的一系列二级子类,这样一套相对固定的等级架构也暴露出来一些问题:

  • • 在实际场景中,主菜和副菜的结合可以是更加灵活多样的,比如作为副菜的鸡蛋不仅可以和主菜的培根组合,还可以和牛肉或者鸡排搭配;比如实际场景中,后续可能有更多的主菜和副菜类型出现,如果需要对所有的组合进行穷尽,则需要经历一轮笛卡尔内积,最终子类的数量将会严重膨胀无法收敛

  • • 使用主菜和副菜对配菜的类型进行界定显得过于刻板,主菜和副菜本质上都是菜品而已,可以根据用户的喜好灵活添加,比如用户可以只要副菜或者只要主菜,可以只添加双份甚至三份的鸡蛋而不添加培根或者牛肉,也可以选择要一份主菜搭配多份配菜,比如一份牛肉两份鸡蛋等等. 这样的话,原本约定好的基于继承实现的等级架构就不再适用了

结合以上两点,我们发现在这类“加料”的场景中,使用继承的设计模式未必合适. 我们不妨切换思路,不再聚焦于尝试对所有组合种类进行一一枚举,而是把注意力放在“加料”的这个过程行为当中:

  • • 首先,我们不再区分主菜和副菜,不论是鸡蛋还是培根还是青椒,我们都把它们当中一种普通的“菜品”

  • • 针对于每一种“菜品”,我们定义出一个装饰器类

  • • 每次使用一个装饰器类时,对应的逻辑是会往原本的主食中添加一份对应的“菜品”

在这种实现的思路下,就诞生出了基于“装饰器模式”的实现架构,如下图所示:

 

在装饰器模式中,一份鸡蛋培根盖浇饭 = 一份白米饭(核心类)+ 一份鸡蛋(装饰器1)+ 一份培根(装饰器2),其中鸡蛋和培根对应装饰器的使用顺序是不作限制的. 于是不管后续有多少种新的“菜品”加入,我们都只需要声明其对应的装饰器类即可,只要“菜品”的种类确定,后续用户想要组装出何种形式的盖浇饭,都是 easy case.

比如,双份鸡蛋盖浇饭 = 一份白米饭(核心类)+ 一份鸡蛋(装饰器1)+一份鸡蛋(装饰器1);鸡蛋火腿青椒盖浇饭 = 一份白米饭(核心类)+ 一份鸡蛋(装饰器1)+一份青椒(装饰器2)+一份火腿(装饰器3);双份牛肉青椒盖浇饭 = 一份白米饭(核心类)+ 一份青椒(装饰器4)+一份牛肉(装饰器5)+一份牛肉(装饰器5)

到这里为止,问题已经得到圆满解决. 下面,我们再回过头对装饰器模式和继承模式做一个对比总结:

  • • 继承强调的是等级制度和子类种类,这部分架构需要一开始就明确好

  • • 装饰器模式强调的是“装饰”的过程,而不强调输入与输出,能够动态地为对象增加某种特定的附属能力,相比于继承模式显得更加灵活,且符合开闭原则

 

2 代码实现

2.1 类型声明实现

下面我们就进入代码实战环节,通过编程的方式实现一个搭配食材的案例,以此来展示装饰器模式的实现细节.

 

这个案例非常简单,我们需要主食的基础上添加配菜,最终搭配出美味可口的食物套餐. 其中主食包括米饭 rice 和面条 noodle 两条,而配菜则包括老干妈 LaoGanMa(老干妈拌饭顶呱呱)、火腿肠 HamSausage 和煎蛋 FriedEgg 三类.

事实上如果需要的话,主食和配菜也可以随时进行扩展,在装饰器模式中,这种扩展行为的成本并不高.

下面先展示一下总体的 UML 类图:

 

首先是对应于装饰器模式中核心类的是原始的主食 Food,我们声明了一个 interface,其中包含两个核心方法,Eat 和 Cost,含义分别为食用主食以及计算出主食对应的花费.

type Food interface {
    // 食用主食
    Eat() string
    // 计算主食的话费
    Cost() float32
}

 

Food 对应的实现类包括米饭 rice 和面条 noodle:

type Rice struct {
}


func NewRice() Food {
    return &Rice{}
}


func (*Rice) Eat() string {
    return "开动了,一碗香喷喷的米饭..."
}


// 需要花费的金额
func (*Rice) Cost() float32 {
    return 1
}


type Noodle struct {
}


func NewNoodle() Food {
    return &Noodle{}
}


func (*Noodle) Eat() string {
    return "嗦面ing...嗦~"
}


// 需要花费的金额
func (*Noodle) Cost() float32 {
    return 1.5
}

 

接下来是装饰器部分,我们声明了一个 Decorate interface,它们本身是在强依附于核心类(主食)的基础上产生的,只能起到锦上添花的作用,因此在构造器函数中,需要传入对应的主食 Food.

type Decorator Food


func NewDecorator(Food) Decorator {
    return f
}

 

接下来分别声明三个装饰器的具体实现类,对应为老干妈 LaoGanMaDecorator、火腿肠 HamSausageDecorator、和煎蛋 FriedEggDecorator.

每个装饰器类的作用是对食物进行一轮装饰增强,因此需要在构造器函数中传入待装饰的食物,然后通过重写食物的 Eat 和 Cost 方法,实现对应的增强装饰效果.

type LaoGanMaDecorator struct {
    Decorator
}


func NewLaoGanMaDecorator(Decorator) Decorator {
    return &LaoGanMaDecorator{
        Decorator: d,
    }
}


func (*LaoGanMaDecorator) Eat() string {
    // 加入老干妈配料
    return "加入一份老干妈~..." + l.Decorator.Eat()
}


func (*LaoGanMaDecorator) Cost() float32 {
    // 价格增加 0.5 元
    return 0.5 + l.Decorator.Cost()
}


type HamSausageDecorator struct {
    Decorator
}


func NewHamSausageDecorator(Decorator) Decorator {
    return &HamSausageDecorator{
        Decorator: d,
    }
}


func (*HamSausageDecorator) Eat() string {
    // 加入火腿肠配料
    return "加入一份火腿~..." + h.Decorator.Eat()
}


func (*HamSausageDecorator) Cost() float32 {
    // 价格增加 1.5 元
    return 1.5 + h.Decorator.Cost()
}


type FriedEggDecorator struct {
    Decorator
}


func NewFriedEggDecorator(Decorator) Decorator {
    return &FriedEggDecorator{
        Decorator: d,
    }
}


func (*FriedEggDecorator) Eat() string {
    // 加入煎蛋配料
    return "加入一份煎蛋~..." + f.Decorator.Eat()
}


func (*FriedEggDecorator) Cost() float32 {
    // 价格增加 1 元
    return 1 + f.Decorator.Cost()
}

 

做好所有的准备工作之后,下面我们通过单测代码,展示装饰器模式的使用示例:

func Test_decorator(*testing.T) {
    // 一碗干净的米饭
    rice := NewRice()
    rice.Eat()


    // 一碗干净的面条
    noodle := NewNoodle()
    noodle.Eat()


    // 米饭加个煎蛋
    rice = NewFriedEggDecorator(rice)
    rice.Eat()


    // 面条加份火腿
    noodle = NewHamSausageDecorator(noodle)
    noodle.Eat()


    // 米饭再分别加个煎蛋和一份老干妈
    rice = NewFriedEggDecorator(rice)
    rice = NewLaoGanMaDecorator(rice)
    rice.Eat()
}

 

2.2 增强函数实现

下面提供另一种闭包实现装饰增强函数的实现示例,其实现也是遵循着装饰器模式的思路,但在形式上会更加简洁直观一些:

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 {
        // 前处理
        fmt.Println("preprocess...")
        err := fn(ctx, param)
        fmt.Println("postprocess...")
        return err
    }
}

其中核心的处理方法 handleFunc 对应的是装饰器模式中的核心类,Decorate 增强方法对应的则是装饰器类,每次在执行 Decorate 的过程中,都会在 handleFunc 前后增加的一些额外的附属逻辑.

 

3 工程案例

为了进一步加深理解,下面摘出一个实际项目中应用到装饰器模式的使用案例和大家共同分析探讨.

这里给到的案例是 grpc-go 中对拦截器链 chainUnaryInterceptors 的实现.

grpc-go 的开源地址:https://github.com/grpc/grpc-go

下面走读的源码版本为 Release 1.53.0

 

在 grpc-go 服务端模块中,每次接收到来自客户端的 grpc 请求,会根据请求的 path 映射到对应的 service 和 handler 进行执行逻辑的处理,但在真正调用 handler 之前,会先先经历一轮对拦截器链 chainUnaryInterceptors 的遍历调用,在这里我们可以把 handler 理解为装饰器模式中的核心类,拦截器链中的每一个拦截器 unaryInterceptors 可以理解为一个装饰器.

下面我们来观察一下其中具体的源码细节.

首先,对于拦截器类 UnaryServerInterceptor,本身是一个函数的类型:

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

UnaryServerInterceptor 对应的几个入参包括:

  • • ctx:golang 请求链路中的上下文,不赘述

  • • req:grpc 请求的入参

  • • info:grpc 业务服务 service

  • • handler:核心逻辑执行方法

 

其中核心逻辑执行方法 handler 对应的类型为 UnaryHandler,入参为 context 和 req,出参为 resp 和 error

type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)

 

下面是生成拦截器链的方法 chainUnaryInterceptors. 该方法的入参是用户定义好的一系列拦截器 interceptors,内部会按照顺序对拦截器进行组装,最终通过层层装饰增强的方式,将整个执行链路压缩成一个拦截器 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))
    }
}

 

在 chainUnaryInterceptors 方法中,闭包返回了一个对应于拦截器 UnaryServerInterceptor 类型的函数. 这个闭包函数内部的执行逻辑是,会调用拦截器列表 interceptors 当中的首个拦截器,并通过 getChainUnaryHandler 方法,依次使用下一枚拦截器对核心方法 handler 进行装饰包裹,封装形成一个新的“handler”供当前的拦截器使用.

 

 

在这个过程中,就体现了我们今天讨论的装饰器模式的设计思路. 核心业务处理方法 handler 对应的就是装饰器模式中的核心类,每一轮通过拦截器 UnaryServerInterceptor 对 handler 进行增强的过程,对应的就是一次“装饰”的步骤.

 

下面给出一个具体实现的装饰器的代码示例,可以看到其中在核心方法 handler 前后分别执行了对应的附属逻辑,起到了装饰的效果.

var myInterceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    // 添加前处理...
    fmt.Printf("interceptor preprocess, req: %+v\n", req)
    resp, err = handler(ctx, req)
    // 添加后处理...
    fmt.Printf("interceptor postprocess, req: %+v\n", resp)
    return
}

 

如果各位读友们想了解更多关于 grpc-go 的内容,可以阅读我之前发表的相关话题文章:

  • • grpc-go 服务端使用介绍及源码分析

  • • grpc-go客户端源码走读

  • • 基于 etcd 实现 grpc 服务注册与发现

 

4 总结

本期和大家交流了设计模式中的装饰器模式. 装饰器模式能够动态地为对象增加某种特定的附属能力,相比于继承模式显得更加灵活,且符合开闭原则,可以作为继承模式的一种有效补充手段.


小徐先生的编程世界
在学钢琴,主业码农
 最新文章