你也是业务开发?提前用这个设计模式预防产品加需求吧

大家好,我是每周在这里陪大家一起进步的网管。

今天继续更新设计模式相关的文章,我在前面两篇关于模板模式和策略模式的文章里给大家说过一个我总结的”暴论”:“模板、策略和职责链三个设计模式是解决业务系统流程复杂多变这个痛点的利器”。这篇文章我们就来一起说说这第三个设计模式利器—职责链模式。

职责链模式

职责链——英文名 Chain of responsibility 有时也被翻译成责任链模式。我看网上叫责任链的更多一些,这里大家知道它们是一个东西就行了。

它是一种行为型设计模式。使用这个模式,我们能为请求创建一条由多个处理器组成的链路,每个处理器各自负责自己的职责,相互之间没有耦合,完成自己任务后请求对象即传递到链路的下一个处理器进行处理。

职责链在很多流行框架里都有被用到,像中间件、拦截器等框架组件都是应用的这种设计模式,这两个组价大家应该用的比较多。在做Web 接口开发的时候,像记录访问日志、解析Token、格式化接口响应的统一结构这些类似的项目公共操都是在中间件、拦截器里完成的,这样就能让这些基础操作与接口的业务逻辑进行解耦。

中间件、拦截器这些组件都是框架给我们设计好的直接往里面套就可以,今天我们的文章里要讲的是,怎么把职责链应用到我们核心的业务流程设计中,而不仅仅是只做那些基础的公共操作。

职责链的价值

上面我们说了职责链在项目公共组件中的一些应用,让我们能在核心逻辑的前置和后置流程中增加一些基础的通用功能。但其实在一些核心的业务中,应用职责链模式能够让我们无痛地扩展业务流程的步骤

比如淘宝在刚刚创立的时候购物生成订单处理流程起初可能是这样的。

你也是业务开发?提前用这个设计模式预防产品加需求吧

职责链模式—购物下单—清纯版

整个流程比较干净“用户参数校验–购物车数据校验–商品库存校验–运费计算–扣库存—生成订单”,我们姑且把它称为清纯版的购物下单流程,这通常都是在产品从0到1的时候,流程比较清纯,在线购物你能实现在线选品、下单、支付这些就行了。

不过大家都是互联网冲浪老手了,也都是吃过见过的主,这个流程要是一直这么清纯,公司的PM和运营就可以走人了。等购物网站跑起来,有消费者了之后,为了提高销售额,一般会加一些,某些品类商品满减的促销手段。

运营也不能闲着,多谈点客户,造个购物节,到时候优惠券安排上多吸引点用户。那这样在下订单的流程中,就得判断购物车里的商品是否满足折扣条件、用户是否有优惠卷,有的话进行金额减免。相当于我们下单的内部流程中间加了两个子流程。

你也是业务开发?提前用这个设计模式预防产品加需求吧

职责链模式—购物下单—老练版

为了实现新加的逻辑,我们就得在写好的订单流程中最起码加两个 if else 分支才能加上这两个逻辑。不过最要命的是因为整个流程耦合在一起,修改了以后我们就得把整个流程全测一遍。并且有了上面的经验我们也应该知道这个流程以后肯定还会扩展,比如再给你加上社群砍一刀、拼单这些功能,以后每次在订单生成流程中加入步骤都得修改已经写好的代码,怕不怕?

有朋友可能会说,互联网电商购物可能确实流程比较多样化,每个公司的流程不一样。我们再举个病人去医院看病的例子,病人看病大体上基本步骤需要有:

挂号—>诊室看病—>收费处缴费—>药房拿药

但是有可能有的病人需要化验、拍片子等等,他们在医院就医的流程可能是这样的:

挂号—>初诊—>影像科拍片—>复诊室—>收费处缴费—>药房拿药

所以就医这个流程也是会根据病人情况的不同,步骤有所增加的。

那么现在我们可以确定:假如一个流程的步骤不固定,为了在流程中增加步骤时,不必修改原有已经开发好,经过测试的流程,我们需要让整个流程中的各个步骤解耦,来增加流程的扩展性,这种时候就可以使用职责链模式啦,这个模式可以让我们先设置流程链路中有哪些步骤,再去执行

用职责链模式实现流程

如果让我们设计责任链应该怎么设计呢?应该提供和实现哪些方法?怎么使用它把流程里的步骤串起来呢?这里我们用职责链模式把就诊看病这个场景中的流程步骤实现一遍给大家做个演示,购物下单的流程类似,咱们下去可以自己尝试实现一遍,先学会职责链模式的结构做些Mock示例,掌握熟练了后面再尝试着用它解决业务中的问题。

首先我们通过上面流程扩展的痛点可以想到,流程中每个步骤都应由一个处理对象来完成逻辑抽象、所有处理对象都应该提供统一的处理自身逻辑的方法,其次还应该维护指向下一个处理对象的引用,当前步骤自己逻辑处理完后,就调用下一个对象的处理方法,把请求交给后面的对象进行处理,依次递进直到流程结束。

总结下来,实现责任链模式的对象最起码需要包含如下特性:

  • 成员属性
    • nextHandler: 下一个等待被调用的对象实例
  • 成员方法
    • SetNext: 把下一个对象的实例绑定到当前对象的nextHandler属性上;
    • Do: 当前对象业务逻辑入口,他是每个处理对象实现自己逻辑的地方;
    • Execute: 负责职责链上请求的处理和传递;它会调用当前对象的DonextHandler不为空则调用nextHandler.Do

如果抽象成 UML 类图表示的话,差不多就是下面这个样子。

你也是业务开发?提前用这个设计模式预防产品加需求吧

定义了一个职责链模式处理对象的接口Handler,由ConcreteHandler –具体处理对象的类型来实现。

观察上图以及上面对象特性的分析,其实是能看出 SetNextExecute 这两个行为是每个 ConcreteHandler 都一样的,所以这两个可以交给抽象处理类型来实现,每个具体处理对象再继承抽象类型,即可减少重复操作。

所以责任链模式的抽象和提炼可以进化成下图这样:

你也是业务开发?提前用这个设计模式预防产品加需求吧

了解完职责链模式从接口和类型设计上应该怎么实现后,我们进入代码实现环节,职责链模式如果用纯面向对象的语言实现起来还是很方便的,把上面的UML类图直接翻译成接口、抽象类,再搞几个实现类就完事。

想把上面这个UML类图翻译成Go代码还是有点难度的。这里咱们提供一个用 Go 实现职责链模式完成医院就诊流程的代码示例。

职责链 Go 代码实现

虽然 Go 不支持继承,不过我们还是能用类型的匿名组合来实现,下面以病人去医院看病这个处理流程为例提供一个具体示例。

看病的具体流程如下:

挂号—>诊室看病—>收费处缴费—>药房拿药

我们的目标是利用责任链模式,实现这个流程中的每个步骤,且相互间不耦合,还支持向流程中增加步骤。

先来实现职责链模式里的公共部分—即模式的接口和抽象类


type PatientHandler interface {
 Execute(*patient) error
 SetNext(PatientHandler) PatientHandler
 Do(*patient) error
}
// 充当抽象类型,实现公共方法,抽象方法不实现留给实现类自己实现
type Next struct {
 nextHandler PatientHandler
}

func (n *Next) SetNext(handler PatientHandler) PatientHandler {
 n.nextHandler = handler
 return handler
}

func (n *Next) Execute(patient *patient) (err error) {
 // 调用不到外部类型的 Do 方法,所以 Next 不能实现 Do 方法
 if n.nextHandler != nil {
  if err = n.nextHandler.Do(patient); err != nil {
   return
  }

  return n.nextHandler.Execute(patient)
 }

 return
}

上面代码中Next类型充当了模式中抽象类的角色,关于这个Next类型这里再重点说明一下。

在我们的职责链的UML图里有说明Do方法是一个抽象方法,留给具体处理请求的类来实现,所以这里Next类型充当抽象类型,只实现公共方法,抽象方法留给实现类自己实现。并且由于 Go 并不支持继承,即使Next实现了Do方法,也不能达到在父类方法中调用子类方法的效果—即在我们的例子里面用Next 类型的Execute方法调用不到外部实现类型的Do方法。

所以我们这里选择Next类型直接不实现Do方法,这也是在暗示这个类型是专门用作让实现类进行内嵌组合使用的。

接下来我们定义职责链要处理的请求,再回看一下我们的UML图,实现处理逻辑和请求传递的DoExecute方法的参数都是流程中要处理的请求。这里是医院接诊的流程,所以我们定义一个患者类作为流程的请求。

//流程中的请求类--患者
type patient struct {
 Name              string
 RegistrationDone  bool
 DoctorCheckUpDone bool
 MedicineDone      bool
 PaymentDone       bool
}

复制

然后我们按照挂号—>诊室看病—>收费处缴费—>药房拿药这个流程定义四个步骤的处理类,来分别实现每个环节的逻辑。


// Reception 挂号处处理器
type Reception struct {
 Next
}

func (r *Reception) Do(p *patient) (err error) {
 if p.RegistrationDone {
  fmt.Println("Patient registration already done")
  return
 }
 fmt.Println("Reception registering patient")
 p.RegistrationDone = true
 return
}

// Clinic 诊室处理器--用于医生给病人看病
type Clinic struct {
 Next
}

func (d *Clinic) Do(p *patient) (err error) {
 if p.DoctorCheckUpDone {
  fmt.Println("Doctor checkup already done")
  return
 }
 fmt.Println("Doctor checking patient")
 p.DoctorCheckUpDone = true
 return
}

// Cashier 收费处处理器
type Cashier struct {
 Next
}

func (c *Cashier) Do(p *patient) (err error) {
 if p.PaymentDone {
  fmt.Println("Payment Done")
  return
 }
 fmt.Println("Cashier getting money from patient patient")
 p.PaymentDone = true
 return
}

// Pharmacy 药房处理器
type Pharmacy struct {
 Next
}

func (m *Pharmacy) Do (p *patient) (err error) {
 if p.MedicineDone {
  fmt.Println("Medicine already given to patient")
  return
 }
 fmt.Println("Pharmacy giving medicine to patient")
 p.MedicineDone = true
 return
}

复制

处理器定义好了,怎么给用他们串成患者就诊这个流程呢?

func main() {
 receptionHandler := &Reception{}
 patient := &patient{Name: "abc"}
 // 设置病人看病的链路
 receptionHandler.SetNext(&Clinic{}).SetNext(&Cashier{}).SetNext(&Pharmacy{})
  receptionHandler.Execute(patient)
}

复制

上面的链式调用看起来是不是很清爽,嘿嘿别高兴太早,这里边有个BUG— 即Reception接诊挂号这个步骤提供的逻辑没有调用到,所以我们这里再定义个StartHandler 类型,它不提供处理实现只是作为第一个Handler向下转发请求


// StartHandler 不做操作,作为第一个Handler向下转发请求
type StartHandler struct {
 Next
}

// Do 空Handler的Do
func (h *StartHandler) Do(c *patient) (err error) {
 // 空Handler 这里什么也不做 只是载体 do nothing...
 return
}

复制

这也是Go 语法限制,公共方法Exeute并不能像面向对象那样先调用this.Do 再调用this.nextHandler.Do 具体原因咱们上边已经解释过了,如果觉得不清楚的可以拿Java实现一遍看看区别,再琢磨一下为啥Go里边不行。

所以整个流程每个环节都能被正确执行到,应该这样把处理类串起来。


func main() {
 patientHealthHandler := StartHandler{}
 //
 patient := &patient{Name: "abc"}
 // 设置病人看病的链路
 patientHealthHandler.SetNext(&Reception{}).// 挂号
  SetNext(&Clinic{}). // 诊室看病
  SetNext(&Cashier{}). // 收费处交钱
  SetNext(&Pharmacy{}) // 药房拿药
 //还可以扩展,比如中间加入化验科化验,图像科拍片等等

 // 执行上面设置好的业务流程
 if err := patientHealthHandler.Execute(patient); err != nil {
  // 异常
  fmt.Println("Fail | Error:" + err.Error())
  return
 }
 // 成功
 fmt.Println("Success")
}

复制

总结

职责链模式所拥有的特点让流程中的每个处理节点都只需关注满足自己处理条件的请求进行处理即可,对于不感兴趣的请求,会直接转发给下一个节点对象进行处理。

另外职责链也可以设置中止条件,针对我们文中的例子就是在Execute方法里加判断,一旦满足中止后就不再继续往链路的下级节点传递请求。Gin 的中间件的abort方法就是按照这个原理实现的,同时这也是职责链跟装饰器模式的一个区别,装饰器模式无法在增强实体的过程中停止,只能执行完整个装饰链路。

后面大家可以看看针对那些可能未来经常会变的核心业务流程,可以在设计初期就考虑使用职责链来实现,减轻未来流程不停迭代时不好扩展的痛点。当然职责链也不是万能的,对于那些固定的流程显然是不适合的。咱们千万不要手里拿着锤子就看什么都是钉子,所有的设计模式一定要用在合适的地方。

既然这里提到了装饰器,那么下一期就写写装饰器吧,不对,装饰器算是代理模式的一个特殊应用,那就还是先介绍代理未来再介绍装饰器吧,这样阅读体验会更好一些。

喜欢这系列文章的朋友们还请多多关注,转发起来吧。

  • END -
本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

那么你的前面两篇在哪里

8小时前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!