Home > Article > Backend Development > Let's talk in depth about the 'Chain of Responsibility Model”, a powerful design pattern tool (with go implementation process)
This article brings you relevant knowledge about golang design patterns. It mainly introduces what the responsibility chain pattern is and its value, as well as the specific implementation method of the responsibility chain Go code. Let’s take a look at it together. Hope it helps those in need.
I will continue to update articles related to design patterns today. In the previous two articles about template patterns and strategy patterns, I told you a "violent theory" that I summarized: "Templates, strategies and The three design patterns of chain of responsibility are a powerful tool to solve the pain point of complex and changeable business system processes." In this article, let’s talk about the third design pattern tool—the chain of responsibility pattern.
applying the chain of responsibility model allows us to painlessly expand the steps of the business process.
For example, when Taobao was first established, the shopping order processing process may have been like this at first. Chain of Responsibility Model—Shopping Order—Pure VersionThe whole process is relatively clean"User parameter verification-shopping cart data verification- Product inventory verification – freight calculation – inventory deduction – order generation”, let’s call it the pure version of the shopping order process. This is usually when the product goes from 0 to 1. The process is relatively pure and online. For shopping, you can just select products, place orders, and pay online.
But we are all veterans of Internet surfing, and we are all familiar with it. If this process remains so pure, the company's PM and operations can leave. After the shopping website is up and running and there are consumers, in order to increase sales, some promotion methods such as full discounts on certain categories of goods will usually be added. Operations can’t be idle either, talk more about customers, create a shopping festival, and then arrange coupons to attract more users. In this way, during the process of placing an order, it is necessary to determine whether the products in the shopping cart meet the discount conditions, whether the user has a coupon, and if so, the amount can be reduced or reduced. It is equivalent to adding two sub-processes in the middle of our internal order process. Chain of Responsibility Model—Place Shopping Order—Experienced VersionIn order to implement the newly added logic, we have to at least write the order process Adding two if else branches can add these two logics. But the most terrible thing is thatbecause the entire process is coupled together, after modification, we have to test the entire process. And with the above experience, we should also know that this process will definitely be expanded in the future. For example, we will add functions such as community cutting and ordering. In the future, every time you add steps to the order generation process, you will have to modify the already written steps. Are you afraid of the code?
Some friends may say that the Internet e-commerce shopping process may indeed be quite diverse, and each company's process is different. Let’s take another example of a patient going to the hospital to see a doctor. Generally speaking, the basic steps for a patient to see a doctor are:Registration—>Seeing a doctor in the clinic—>Paying at the charge office—>Getting medicine at the pharmacy
But there may be patients who need tests, films, etc. The process of their medical treatment in the hospital may be as follows:
Registration—>Initial consultation—>Radio imaging department—> ;Re-examination room—>Payment at the charge office—>Pick up medicine at the pharmacy
Therefore, the process of medical treatment will also have more steps depending on the patient’s condition.
Now we can be sure: If the steps of a process are not fixed, in order to add steps to the process without modifying the original developed and tested process, we need to make the entire process Decoupling the various steps to increase the scalability of the process. In this case, you can use the chain of responsibility model. This model allows us to first set the steps in the process link and then execute .
If we are asked to design the responsibility chain, how should we design it? What methods should be provided and implemented? How to use it to connect the steps in the process? Here we use the chain of responsibility model to implement the process steps in the scenario of seeing a doctor to give you a demonstration. The process of shopping and placing an order is similar. We can try to implement it ourselves. First, learn the structure of the chain of responsibility model and make some Mock examples. Once you have mastered it, you can try to use it to solve business problems.
First of all, we can think of the pain points of the above process expansion. Each step in the process should be completed by a processing object to complete the logical abstraction. All processing objects should provide a unified method of processing their own logic. Secondly, they should also Maintain a reference to the next processing object. After the current step's own logic is processed, the processing method of the next object is called, and the request is handed over to the subsequent object for processing, and the process proceeds in sequence until the end of the process.
To sum up, objects that implement the chain of responsibility pattern need to contain at least the following characteristics:
nextHandler
: Next wait Called object instanceSetNext
: Bind the next object instance to the current object’s nextHandler
Attributes; Do
: The current object business logic entry, which is where each processing object implements its own logic; Execute
: Responsible for the processing and delivery of requests on the responsibility chain; it will call Do
of the current object. If nextHandler
is not empty, it will call nextHandler.Do
; If abstracted into a UML class diagram, it would look like the following.
defines an interface Handler
for chain of responsibility mode processing objects, which is implemented by ConcreteHandler
– the type of specific processing object.
Observing the above picture and the analysis of the above object characteristics, we can actually see that the two behaviors of SetNext
and Execute
are each ConcreteHandler
They are all the same, so these two can be implemented by abstract processing types, and each specific processing object inherits the abstract type to reduce repeated operations.
So the abstraction and refinement of the chain of responsibility pattern can evolve into the following figure:
After understanding how the chain of responsibility pattern should be implemented in terms of interface and type design Finally, we enter the code implementation phase. The chain of responsibility model is very convenient to implement in a pure object-oriented language. Just translate the above UML class diagram directly into interfaces and abstract classes, and then create a few implementation classes and you're done.
It is still a bit difficult to translate the above UML class diagram into Go code. Here we provide a code example that uses Go to implement the chain of responsibility model to complete the hospital treatment process.
Although Go does not support inheritance, we can still use anonymous combinations of types To achieve this, a specific example is provided below, taking the process of patients going to the hospital for medical treatment as an example.
The specific process of medical treatment is as follows:
Registration—>Seeing a doctor in the clinic—>Paying at the fee office—>Getting medicine at the pharmacy
Ours The goal is to use the chain of responsibility model to implement each step in this process without coupling to each other, and to support adding steps to the process.
First, let’s implement the common parts in the chain of responsibility pattern—that is, the interface and abstract class of the pattern
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}
The Next
type in the above code acts as the role of the abstract class in the pattern , let’s focus on this Next
type here.
在我们的职责链的UML图里有说明Do
方法是一个抽象方法,留给具体处理请求的类来实现,所以这里Next
类型充当抽象类型,只实现公共方法,抽象方法留给实现类自己实现。并且由于 Go 并不支持继承,即使Next
实现了Do
方法,也不能达到在父类方法中调用子类方法的效果—即在我们的例子里面用Next
类型的Execute
方法调用不到外部实现类型的Do
方法。
所以我们这里选择Next
类型直接不实现Do
方法,这也是在暗示这个类型是专门用作让实现类进行内嵌组合使用的。
接下来我们定义职责链要处理的请求,再回看一下我们的UML图,实现处理逻辑和请求传递的Do
、Execute
方法的参数都是流程中要处理的请求。这里是医院接诊的流程,所以我们定义一个患者类作为流程的请求。
//流程中的请求类--患者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的Dofunc (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
方法就是按照这个原理实现的,同时这也是职责链跟装饰器模式的一个区别,装饰器模式无法在增强实体的过程中停止,只能执行完整个装饰链路。
后面大家可以看看针对那些可能未来经常会变的核心业务流程,可以在设计初期就考虑使用职责链来实现,减轻未来流程不停迭代时不好扩展的痛点。当然职责链也不是万能的,对于那些固定的流程显然是不适合的。咱们千万不要手里拿着锤子就看什么都是钉子,所有的设计模式一定要用在合适的地方。
既然这里提到了装饰器,那么下一期就写写装饰器吧,不对,装饰器算是代理模式的一个特殊应用,那就还是先介绍代理未来再介绍装饰器吧,这样阅读体验会更好一些。
喜欢这系列文章的朋友们还请多多关注,转发起来吧。
推荐学习:《go视频教程》
The above is the detailed content of Let's talk in depth about the 'Chain of Responsibility Model”, a powerful design pattern tool (with go implementation process). For more information, please follow other related articles on the PHP Chinese website!