大家好,我是网管,每周陪你一起进步。今天继续更新设计模式相关的文章。在前面两篇关于模板模式和策略模式的文章中,我给大家讲了一个我总结的“暴力论”:“模板、策略、责任链这三种设计模式是解决业务问题痛点的利器”点复杂多变的系统流程。”在这篇文章中,我们来说说第三种设计模式武器——责任链模式。责任链模式ChainofResponsibility——英文名称Chainofresponsibility有时翻译成责任链模式,我看责任链比较多在网上,大家也都知道他们是一个东西,是一种行为设计模式,利用这种模式,我们可以为请求创建一个由多个处理器组成的链接,每个处理器各司其职,有它们之间没有耦合,在完成自己的任务后,将请求对象传递给链接中的下一个环节,处理器去处理。责任链在很多流行的框架中使用,中间件、拦截器等框架组件都是应用的设计模式.这两个组件应该多用,在做web界面开发的时候,类似项目常用的操作,比如记录access日志、解析Tokens、格式化接口响应的统一结构,都是在中间件和拦截器中完成的,让这些基本操作和接口业务逻辑解耦。中间件、拦截器等组件都是框架为我们设计的,可以直接插入其中。我们今天这篇文章要讲的是如何将责任链应用到我们的核心业务流程设计中,而不仅仅是做那些基础的公共操作。责任链的价值上面我们提到了责任链在项目公共组件中的一些应用,可以让我们在核心逻辑的前处理和后处理中添加一些基本的通用功能。但实际上,在一些核心业务中,责任链模型的应用可以让我们无痛地扩展业务流程的步骤。比如淘宝刚成立的时候,购物和生成订单的订单处理流程,一开始可能是这样的。ChainofResponsibilitymode—shoppingorder—pureversion,整个流程比较干净,“用户参数验证——购物车数据验证——商品库存验证——运费计算——扣除库存——生成订单”,暂且称之为纯版的购物下单流程,这个一般是商品从0到1的时候,流程比较纯。在线购物允许您在线选择产品、下订单和支付。但是大家都是上网的老手,都是见多识广的高手。如果这个过程总是那么纯粹,公司的PM和运营就可以走了。购物网站运行起来有了消费者之后,为了增加销量,一般会加入一些促销手段,比如对某些品类的产品进行全额折扣。运营不能闲着,多谈客户,搞个购物节,再安排优惠券,吸引更多用户。这样,在下单的过程中,需要判断购物车中的商品是否满足折扣条件,用户是否有优惠券,如果有,则减少金额。相当于在我们内部的下单流程中间加了两个子流程。责任链模式——购物订单——精进版为了实现新增的逻辑,我们至少要在写订单流程中添加至少两个ifelse分支来添加这两个逻辑。但是最要命的是,因为整个流程是耦合在一起的,我们修改之后还要对整个流程进行测试。而且有了上面的经验,我们也应该知道,这个流程以后肯定会扩展,比如增加社区剁手、下单等功能。以后每次在订单生成流程中增加步骤,都得修改已经写好的。你害怕代码吗?可能有朋友会说,互联网电商购物可能确实有一个比较多样化的流程,每个公司的流程都不一样。再举一个病人去医院就医的例子。患者就诊的基本步骤一般包括:挂号—>门诊就诊—>收费站缴费—>药房取药,但部分患者可能需要化验、拍片等,他们在医院看病的流程可能是这样的:挂号—>初诊—>影像科拍片—>复诊室—>收费站缴费—>药房取药。因此,看病的流程也会根据患者的情况有所增加。.所以现在我们可以确定:如果一个流程的步骤不是固定的,为了在不修改原来开发测试的流程的情况下给流程增加步骤,我们需要将整个流程中的步骤解耦,增加流程中的这个在这种情况下,可以使用责任链模式。这种模式允许我们先在流程链接中设置步骤,然后再执行它们。如果让我们设计责任链来实现流程,我们应该如何设计责任链呢?应该提供和实施哪些方法?如何用它把流程中的步骤串起来?这里我们使用责任链模型来实现看病场景中的流程步骤,给大家演示一下。下订单的过程是相似的。让我们尝试自己实现它。首先,学习责任链模型的结构并做一些模拟示例。掌握之后,尝试用它来解决业务问题。首先,通过上面流程扩展的痛点,我们可以认为流程中的每一步都应该由一个处理对象来完成逻辑抽象,所有的处理对象都应该提供一个统一的方法来处理自己的逻辑,其次,他们还应该维护指向下一个处理对象的指针当前步骤的逻辑处理完成后,调用下一个对象的处理方法,并将请求交给后续对象处理,并且过程是递进的,直到过程结束。综上所述,实现责任链模式的对象至少需要包括以下特性:成员属性nextHandler:等待调用的对象实例的next成员方法SetNext:将下一个对象实例绑定到nextHandler属性当前对象的;Do:当前对象的业务逻辑入口,是每个处理对象实现自己逻辑的地方;Execute:负责责任链上请求的处理和传递;会调用当前对象的Do,如果nextHandler不为空,则调用nextHandler.Do;如果将其抽象成一个UML类图,则如下所示。定义了一个接口Handler,用于处理责任链模式下的对象,由ConcreteHandler实现——具体处理对象的类型。观察上图和上面对象特性的分析,其实我们可以看出,SetNext和Execute这两个行为对于每一个ConcreteHandler都是一样的,所以这两个可以通过抽象的处理类型来实现,每个具体的处理对象然后继承抽象类型,减少重复操作。因此,责任链模型的抽象和细化可以演变为下图:在接口和类型设计上理解了责任链模型应该如何实现之后,我们进入代码实现环节。如果责任链模型是用纯面向对象语言实现的,还是很方便的,直接把上面的UML类图翻译成接口和抽象类,然后创建几个实现类就大功告成了。将上面的UML类图翻译成Go代码还是有点难度的。这里我们提供一个代码示例,使用Go实现责任链模型来完成就诊流程。责任链Go代码实现虽然Go不支持继承,但是我们仍然可以通过类型的匿名组合来实现。下面以患者去医院的过程为例,提供一个具体的例子。就诊的具体流程为:挂号—>诊室就诊—>收费站缴费—>药店取药。我们的目标是使用责任链模型来实现这个过程中的每一步,并且彼此之间没有耦合。添加步骤。让我们首先实现责任链模式的公共部分——即模式的接口和抽象类。”本文使用的完整可运行源码可发送至公众号“网络管理”接收【设计模式】"typePatientHandlerinterface{Execute(*patient)errorSetNext(PatientHandler)PatientHandlerDo(*patient)error}//充当抽象类型,实现公共方法,如果抽象方法没有实现,则交给实现类去实现nextHandler=handlerreturnhandler}func(n*Next)Execute(patient*patient)(errerror){//不能调用外部类型的Do方法,所以Next不能实现Do方法ifn.nextHandler!=nil{iferr=n.nextHandler.Do(patient);err!=nil{return}returnn.nextHandler.Execute(patient)}return}上面代码中的Nexttype作为模式中的抽象类的作用这个Next类型这里再解释一下,在我们责任链的UML图中,说明Do方法是一个抽象方法,留给具体处理请求的类。所以这里Next类型作为一个抽象类型,只实现了公共方法,抽象方法留给实现类自己处理。并且由于Go不支持继承,即使Next实现了Do方法,也无法达到在父类方法中调用子类方法的效果——也就是说,在我们的例子中,Next类型的Execute方法无法调用外部实现类型的Do方法。所以我们这里选择Next类型,并没有直接实现Do方法,这也暗示了这个类型是专门用来让实现类与embedded结合使用的。接下来我们定义责任链要处理的请求,再回头看我们的UML图实现处理逻辑,请求传递的Do和Execute方法的参数都是流程中要处理的请求。这里是入院的流程,所以我们定义一个patientclass作为对流程的请求。//流程中的请求类--patienttypepatientstruct{NamestringRegistrationDoneboolDoctorCheckUpDoneboolMedicineDoneboolPaymentDonebool}然后我们定义注册流程中的四个步骤—>看病看病—>缴费office—>pharmacyProcessing类取药分别实现各个环节的逻辑。"本文用到的完整可运行源码可以发到公众号"网络管理"发送【设计模式】获取"//ReceptionRegisteredprocessortypeReceptionstruct{Next}func(r*Reception)Do(p*patient)(errerror){ifp.RegistrationDone{fmt.Println("Patientregistrationalreadydone")return}fmt.Println("Receptionregisteringpatient")p.RegistrationDone=truereturn}//诊所处理器--供医生看病使用。typeClinicstruct{Next}func(d*Clinic)Do(p*patient)(errerror){ifp.DoctorCheckUpDone{fmt.Println("Doctorcheckupalreadydone")return}fmt.Println("医生检查病人")p.DoctorCheckUpDone=truereturn}//收银员收费处理器类型Cashierstruct{Next}func(c*Cashier)Do(p*patient)(errerror){ifp.PaymentDone{fmt.Println("PaymentDone")return}fmt.Println("Cashiergettingmoneyfrompatientpatient")p.PaymentDone=truereturn}//Pharmacypharmacy处理器类型Pharmacystruct{Next}func(m*Pharmacy)Do(p*patient)(errerror){ifp.MedicineDone{fmt.Println("药物已经给病人")return}fmt.Println("Pharmacygivingmedicinetopatient")p.MedicineDone=truereturn}定义了processors,如何使用它们组成患者看病的流程?funcmain(){receptionHandler:=&Reception{}patient:=&patient{Name:"abc"}//设置患者看病链接receptionHandler.SetNext(&Clinic{}).SetNext(&Cashier{}).SetNext(&Pharmacy{})receptionHandler.Execute(patient)}上面的链式调用是不是看起来很爽快,哎,别高兴得太早,这里有个BUG——就是Recepiton注册这一步提供的逻辑没有被调用,所以我们这里又定义了一个StartHandler类型,不提供处理实现,而是作为第一个Handler向下转发请求。"本文使用的完整可运行源码去公众号"网络管理叮咚"发送【设计模式】获取"//StartHandler不操作,将请求转发为第一个Handler类型StartHandlerstruct{Next}//DofuncofDoemptyHandler(h*StartHandler)Do(c*patient)(errerror){//EmptyHandlerhereDonothingbutcarrierdonothing...return}这也是Go语法的限制,publicmethodExeute不能像面向对象一样先调用this.Do再调用this.nextHandler.Do。具体原因我们上面已经解释过了,如果觉得不清楚,可以用Java实现看看区别,然后再想想为什么在Go里行不通,这样整个流程的每一个环节都能正确执行,并且处理类应该以这种方式连接。"本文用到的完整可运行源码可以发到公众号「网管必唯」【设计模式】获取"funcmain(){patientHealthHandler:=StartHandler{}//patient:=&patient{Name:"abc"}//设置患者看病链接patientHealthHandler.SetNext(&Reception{}).//RegistrationSetNext(&Clinic{}).//诊室SetNext(&Cashier{}).//收费处缴费SetNext(&Pharmacy{})//药店收款//也可以扩展,比如中间增加化验,影像科拍片等//err则执行上面设置的业务流程:=patientHealthHandler.Execute(患者);err!=nil{//Exceptionfmt.Println("Fail|Error:"+err.Error())return}//Successfmt.Println("Success")}本文完整源码已包含在我组织的电子教程。可以将关键字【设计模式】发送到我的公众号「网管毕向」接收。总结责任链模型的特点,让流程中的每个处理节点只需要关注满足自身处理条件的请求进行处理,不感兴趣的请求会直接转发给下一个节点对象进行处理。此外,责任链还可以设置暂停条件。我们文章中的例子,在Execute方法中加入了判断。一旦满足暂停,请求将不会继续传输到链路的下级节点。Gin的中间件的abort方法就是按照这个原理实现的。同时,这也是责任链和装饰者模式的区别。装饰者模式在增强实体的过程中不能停止,只能执行整个装饰环节。后面可以看一下未来可能经常变化的核心业务流程。可以考虑在设计初期使用责任链,缓解以后流程迭代时不易扩展的痛点。当然,责任链也不是万能的,显然不适合那些固定的流程。我们不能手里拿着锤子,看到什么都是钉子。所有的设计模式都必须用在合适的地方。既然这里提到了装饰器,那我们下一期再写装饰器吧。不是,装饰器是代理模式的一种特殊应用,先介绍代理再介绍装饰器,这样阅读体验会更好。.
