图片来自 Pexels
“你这代码写的像坨屎”,今天我的代码又被当作典型被 CTO 骂了......于是他给我的建议如下:
请求在一个链条上处理,链条上的受理者处理完毕之后决定是继续往后传递还是中断当前处理流程。
适用于多节点的流程处理,每个节点完成各自负责的部分,节点之间不知道彼此的存在,比如 OA 的审批流,Java Web 开发中的 Filter 机制。
举一个生活中的例子,笔者之前租房的时候遇到了所谓的黑中介,租的时候感觉自己是上帝,但是坏了东西找他修的时候就像个孙子一样。
中介让我找门店客服,门店客服又让我找房东,房东又让我找她家老公,最终好说歹说才把这事了了(租房一定要找正规中介)。
笔者目前所做的业务是校园团餐的聚合支付,业务流程很简单:
学生打开手机付款码支付。
食堂大妈使用机具扫付款码收款。
大学食堂有个背景是这样的,食堂有补贴,菜品比较便宜,所以学校是不愿意让社会人士去学校食堂消费的,鉴于此,我们在支付之前加了一套是否允许支付的检验逻辑。
大体如下:
某档口只允许某类用户用户消费,比如教师档口只允许教师消费,学生档口不允许校外用户消费。
某个档口一天只允许某类用户消费几次,比如教师食堂一天只允许学生消费一次。
针对这几类情况我建立了三类过滤器,分别是:
SpecificCardUserConsumeLimitFilter:按用户类型判断是否允许消费。
DayConsumeTimesConsumeLimitFilter:按日消费次数判断是否允许消费。
MuslimConsumeLimitFilter:非清真用户是否允许消费。
判断逻辑是先通过 SpecificCardUserConsumeLimitFilter 判断当前用户是否可以在此档口消费。
如果允许继续由 DayConsumeTimesConsumeLimitFilter 判断当天消费次数是否已用完;如果未用完继续由 MuslimConsumeLimitFilter 判断当前用户是否满足清真餐厅的就餐条件,前面三条判断,只要有一个不满足就提前返回。
总结:将每种限制条件的判断逻辑封装到了具体的 Filter 中,如果某种限制条件的逻辑有修改不会影响其他条件,如果需要新加限制条件只需要重新构造一个 Filter 织入到 FilterChain 上即可。
定义一系列的算法,把每一个算法封装起来,并且使它们可相互替换。
主要是为了消除大量的 if else 代码,将每种判断背后的算法逻辑提取到具体的策略对象中,当算法逻辑修改时对使用者无感知,只需要修改策略对象内部逻辑即可。
这类策略对象一般都实现了某个共同的接口,可以达到互换的目的。
笔者之前有个需求是用户扫码支付以后向档口的收银设备推送一条支付消息,收银设备收到消息以后会进行语音播报,逻辑很简单,就是调用推送平台推送一条消息给设备即可。
但是由于历史原因,某些设备对接的推送平台是不一样的,A 类设备优先使用信鸽推送,如果失败了需要降级到长轮询机制,B 类设备直接使用自研的推送平台即可。
还有个现状是 A 类和 B 类的消息格式是不一样的(不同的团队开发,后期被整合到一起)。
鉴于此,我抽象出 PushStrategy 接口,其具体的实现有 IotPushStrategy 和 XingePushStrategy,分别对应自研推送平台的推送策略和信鸽平台的推送策略,使用者时针对不同的设备类型使用不同的推送策略即可。
总结:将每种通道的推送逻辑封装到了具体的策略中,某种策略的变更不会影响其他策略,由于实现了共同接口,所以策略可以互相替换,对使用者友好。
比如 Java ThreadPoolExecutor 中的任务拒绝策略,当线程池已经饱和的时候会执行拒绝策略,具体的拒绝逻辑被封装到了 RejectedExecutionHandler 的 rejectedExecution 中。
模板的价值就在于骨架的定义,骨架内部将问题处理的流程已经定义好,通用的处理逻辑一般由父类实现,个性化的处理逻辑由子类实现。
比如炒土豆丝和炒麻婆豆腐,大体逻辑都是:
切菜
放油
炒菜
出锅
1,2,4 都差不多,但是第 3 步是不一样的,炒土豆丝得拿铲子翻炒,但是炒麻婆豆腐得拿勺子轻推,否则豆腐会烂(疫情宅在家,学了不少菜)。
不同场景的处理流程,部分逻辑是通用的,可以放到父类中作为通用实现,部分逻辑是个性化的,需要子类去个性实现。
还是接着之前语音播报的例子来说,后期我们新加了两个需求:
消息推送需要增加 trace。
有些通道推送失败需要重试。
所以现在的流程变成了这样:
trace 开始。
通道开始推送。
是否允许重试,如果允许执行重试逻辑。
trace 结束。
其中 1 和 4 是通用的,2 和 3 是个性化的,鉴于此我在具体的推送策略之前增加了一层父类的策略,将通用逻辑放到了父类中。
总结:通过模板定义了流程,将通用逻辑放在父类实现,减少了重复代码,个性化逻辑由子类自己实现,子类间修改代码互不干扰也不会破坏流程。
顾名思义,此模式需要有观察者(Observer)和被观察者(Observable)两类角色。
当 Observable 状态变化时会通知 Observer,Observer 一般会实现一类通用的接口。
比如 java.util.Observer,Observable 需要通知 Observer 时,逐个调用 Observer 的 update 方法即可,Observer 的处理成功与否不应该影响 Observable 的流程。
一个对象(Observable)状态改变需要通知其他对象,Observer 的存在不影响 Observable 的处理结果,Observer 的增删对 Observable 无感知。
比如 Kafka 的消息订阅,Producer 发送一条消息到 Topic,至于是 1 个还是 10 个 Consumer 订阅这个 Topic,Producer 是不需要关注的。
在责任链设计模式那块我通过三个 Filter 解决了消费限制检验的问题,其中有一个 Filter 是用来检验消费次数的,我这里只是读取用户的消费次数,那么消费次数的累加是怎么完成的呢?
其实累加这块就用到了观察者模式,具体来讲是这样,当交易系统收到支付成功回调时会通过 Spring 的事件机制发布“支付成功事件”。
这样负责累加消费次数和负责语音播报的订阅者就会收到“支付成功事件”,进而做各自的业务逻辑。
总结:观察者模式将被观察者和观察者之间做了解耦,观察者存在与否不会影响被观察者的现有逻辑。
装饰器用来包装原有的类,在对使用者透明的情况下做功能的增强,比如 Java 中的 BufferedInputStream 可以对其包装的 InputStream 做增强,从而提供缓冲功能。
希望对原有类的功能做增强,但又不希望增加过多子类时,可以使用装饰器模式来达到同样的效果。
笔者之前在推动整个公司接入 trace 体系,因此也提供了一些工具来解决 trace 的自动织入和上下文的自动传递。
为了支持线程间的上下文传递,我增加了 TraceRunnableWrapper 这个装饰类,从而起到将父线程的上下文透传到子线程中,对使用者完全透明。
总结:使用装饰器模式做了功能的增强,对使用者来说只需要做简单的组合就能继续使用原功能。
何为外观,就是对外提供一个统一的入口:
一是可以影藏系统内部的细节。
二是可以降低使用者的复杂度。
比如 SpringMVC 中的 DispaterServlet,所有的 Controller 都是通过 DispaterServlet 统一暴露。
降低使用者的复杂度,简化客户端的接入成本。
笔者所在的公司对外提供了一些开放能力给第三方 ISV,比如设备管控、统一支付、对账单下载等能力。
由于分属于不同的团队,所以对外提供的接口形式各异,初期还好,接口不多,ISV 也能接受,但是后期接口多了 ISV 就开始抱怨接入成本太高。
为了解决这一问题,我们在开放接口前面加了一层前端控制器 GatewayController,其实就是我们后来开放平台的雏形。
GatewayController 对外统一暴露一个接口 gateway.do,将对外接口的请求参数和响应参数统一在 GatewayController 做收敛,GatewayController 往后端服务路由时也采用统一接口。
总结:采用外观模式屏蔽了系统内部的一些细节,降低了使用者的接入成本。
就拿 GatewayController 来说,ISV 的鉴权,接口的验签等重复工作统一由它实现。
ISV 对接不同的接口只需要关心一套接口协议接口,由 GatewayController 这一层做了收敛。
往期热门文章:
本文分享自微信公众号 - 架构师修炼(jiagouxiulian)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。