设计模式在我工作中的巧妙实践
设计模式在我工作中的实践
大多数时候我都是写一些业务代码,可能一堆 CRUD 就能解决问题,但是这样的工作对技术人的提升并不多,如何让自己从业务中解脱出来找到写代码的乐趣呢,我做过一些尝试,使用设计模式改善自己的业务代码就是其中的一种。
设计模式实践
责任链设计模式
责任链设计模式定义
请求在一个链条上处理,链条上的受理者处理完毕之后决定是继续往后传递还是中断当前处理流程。
适用场景
适用于多节点的流程处理,每个节点完成各自负责的部分,节点之间不知道彼此的存在,比如 OA 的审批流,java web 开发中的 Filter 机制。
举一个生活中的例子,笔者之前租房的时候遇到了所谓的黑中介,租的时候感觉自己是上帝,但是坏了东西找他修的时候就像个孙子一样,中介让我找门店客服,门店客服又让我找房东,房东又让我找她家老公,最终好说歹说才把这事了了(租房一定要找正规中介)。
实践经验
笔者目前所做的业务是校园团餐的聚合支付,业务流程很简单:
- 学生打开手机付款码支付。
- 食堂大妈使用机具扫付款码收款。
大学食堂有个背景是这样的,食堂有补贴,菜品比较便宜,所以学校是不愿意让社会人士去学校食堂消费的,鉴于此,我们在支付之前加了一套是否允许支付的检验逻辑,大体如下:
- 某档口只允许某类用户用户消费,比如教师档口只允许教师消费,学生档口不允许校外用户消费;
- 某个档口一天只允许某类用户消费几次,比如教师食堂一天只允许学生消费一次;
- 是否允许非清真学生消费,比如某些清真餐厅,是不允许非清真学生消费的;
针对这几类情况我建立了三类过滤器,分别是:
SpecificCardUserConsumeLimitFilter
:按用户类型判断是否允许消费
DayConsumeTimesConsumeLimitFilter
:按日消费次数判断是否允许消费
MuslimConsumeLimitFilter
:非清真用户是否允许消费
判断逻辑是先通过SpecificCardUserConsumeLimitFilter
判断当前用户是否可以在此档口消费,如果允许继续由DayConsumeTimesConsumeLimitFilter
判断当天消费次数是否已用完,如果未用完继续由MuslimConsumeLimitFilter
判断当前用户是否满足清真餐厅的就餐条件,前面三条判断,只要有一个不满足就提前返回。
部分代码如下:
public boolean canConsume(String uid, String shopId, String supplierId) {/* 获取用户信息,用户信息包含类型(student:学生,teacher:老师,unknown:未知用户)、名族(han:汉族,mg:蒙古族) */UserInfo userInfo = getUserInfo(uid);/* 获取消费限制信息,限制信息包含是否允许非清真消费、每种类型的用户是否允许消费以及允许消费的次数 */ConsumeConfigInfo consumeConfigInfo = getConsumeConfigInfo(shopId, supplierId)/* 构造消费限制过滤器链条 */ConsumeLimitFilterChain filterChain = new ConsumeLimitFilterChain();filterChain.addFilter(new SpecificCardUserConsumeLimitFilter());filterChain.addFilter(new DayConsumeTimesConsumeLimitFilter());filterChain.addFilter(new MuslimConsumeLimitFilter());boolean checkResult = filterChain.doFilter(filterChain, schoolMemberInfo, consumeConfigInfo);}/* filterChain.doFilter方法 */public boolean doFilter(ConsumeLimitFilterChain filterChain, UserInfo userInfo,ConsumeConfigInfo consumeConfigInfo) {/* 迭代调用过滤器 */if (index < filters.size()) {return (filters.get(index++).doFilter(filterChain, userInfo, consumeConfigInfo));}}/* SpecificCardUserConsumeLimitFilter.doFilter方法 */public boolean doFilter(ConsumeLimitFilterChain filterChain, UserInfo userInfo,ConsumeConfigInfo consumeConfigInfo) {/* 获取某一类型的消费限制,比如student允许消费,unknown不允许消费 */CardConsumeConfig cardConsumeConfig = findSuitCardConfig(userInfo, consumeConfigInfo);/* 判断当前卡用户是否允许消费 */if (consumeCardConfig != null) {if ((!CAN_PAY.equals(cardConsumeConfig.getEnabledPay()))) {return (false);}}/* 其余情况,继续往后传递 */return (filterChain.doFilter(filterChain, memberInfo, consumeConfig));}/* DayConsumeTimesConsumeLimitFilter.doFilter方法 */public boolean doFilter(ConsumeLimitFilterChain filterChain, UserInfo userInfo,ConsumeConfigInfo consumeConfigInfo) {/* 获取某一类型的消费限制,比如student可以消费2次 */CardConsumeConfig cardConsumeConfig = findSuitCardConfig(userInfo, consumeConfigInfo);/* 获取当前用户今天的消费次数 */int consumeCnt = getConsumeCnt(userInfo)if (consumeCnt >= cardConsumeConfig.getDayConsumeTimesLimit()) {return (false);}/* 其余情况,继续往后传递 */return (filterChain.doFilter(filterChain, memberInfo, consumeConfig));}
总结
将每种限制条件的判断逻辑封装到了具体的Filter
中,如果某种限制条件的逻辑有修改不会影响其他条件,如果需要新加限制条件只需要重新构造一个Filter
织入到FilterChain
上即可。
策略设计模式
策略设计模式定义
定义一系列的算法,把每一个算法封装起来, 并且使它们可相互替换
适用场景
主要是为了消除大量的 if else 代码,将每种判断背后的算法逻辑提取到具体的策略对象中,当算法逻辑修改时对使用者无感知,只需要修改策略对象内部逻辑即可。这类策略对象一般都实现了某个共同的接口,可以达到互换的目的。
实践经验
笔者之前有个需求是用户扫码支付以后向档口的收银设备推送一条支付消息,收银设备收到消息以后会进行语音播报,逻辑很简单,就是调用推送平台推送一条消息给设备即可,但是由于历史原因,某些设备对接的推送平台是不一样的:
- A 类设备优先使用信鸽推送,如果失败了需要降级到长轮询机制
- B 类设备直接使用自研的推送平台即可
还有个现状是 A 类和 B 类的消息格式是不一样的(不同的团队开发,后期被整合到一起),鉴于此,我抽象出PushStrategy
接口,其具体的实现有IotPushStrategy
和XingePushStrategy
,分别对应自研推送平台的推送策略和信鸽平台的推送策略,使用者时针对不同的设备类型使用不同的推送策略即可。部分代码如下:
/*** 推送策略*/
public interface PushStrategy {/**@param deviceVO设备对象,包扣设备sn,信鸽pushid@param content,推送内容,一般为json*/public CallResult push(AppDeviceVO deviceVO, Object content);
}class IotPushStrategy implements PushStrategy{/**@param deviceVO设备对象,包扣设备sn,信鸽pushid@param content,推送内容,一般为json*/public CallResult push(AppDeviceVO deviceVO, Object content){//创建自研推送平台需要的推送报文Message message = createPushMsg(deviceVO,content);//调用推送平台推送接口IotMessageService.pushMsg(message);}
}class XingePushStrategy implements PushStrategy{/**@param deviceVO设备对象,包扣设备sn,信鸽pushid@param content,推送内容,一般为json*/public CallResult push(AppDeviceVO deviceVO, Object content){//创建信鸽平台需要的推送报文JSONObject jsonObject = createPushMsg(content);//调用推送平台推送接口if(!XinggePush.pushMsg(message)){//降级到长轮询...}}
}/**
消息推送Service
*/
MessagePushService{pushMsg(AppDeviceVO deviceVO, Object content){if(A设备){XingePushStrategy.push(deviceVO,content);} else if(B设备){IotPushStrategy.push(deviceVO,content);}}
}
总结
将每种通道的推送逻辑封装到了具体的策略中,某种策略的变更不会影响其他策略,由于实现了共同接口,所以策略可以互相替换,对使用者友好,比如ThreadPoolExecutor
中的任务拒绝策略,当线程池已经饱和的时候会执行拒绝策略,具体的拒绝逻辑被封装到了RejectedExecutionHandler
的rejectedExecution
中。
推荐阅读:SpringBoot如何使用策略模式干掉if else
模板设计模式
模板设计模式定义
模板的价值就在于骨架的定义,骨架内部将问题处理的流程已经定义好,通用的处理逻辑一般由父类实现,个性化的处理逻辑由子类实现。
比如炒土豆丝和炒麻婆豆腐,大体逻辑都是 1.切菜,2.放油,3.炒菜,4.出锅,1,2,4 都差不多,但是第 3 步是不一样的,炒土豆丝得拿铲子翻炒,但是炒麻婆豆腐得拿勺子轻推,否则豆腐会烂(疫情宅在家,学了不少菜)。
使用场景
不同场景的处理流程,部分逻辑是通用的,可以放到父类中作为通用实现,部分逻辑是个性化的,需要子类去个性实现。
实践经验
还是接着之前语音播报的例子来说,后期我们新加了两个需求:
- 消息推送需要增加 trace
- 有些通道推送失败需要重试
所以现在的流程变成了这样:
- trace 开始
- 通道开始推送
- 是否允许重试,如果允许执行重试逻辑
- trace 结束
其中 1 和 4 是通用的,2 和 3 是个性化的,鉴于此我在具体的推送策略之前增加了一层父类的策略,将通用逻辑放到了父类中,修改后的代码如下:
abstract class AbstractPushStrategy implements PushStrategy{@Overridepublic CallResult push(AppDeviceVO deviceVO, Object content) {//1.构造spanSpan span = buildSpan();//2.具体通道推送逻辑由子类实现CallResult callResult = doPush(deviceVO, content);//3.是否允许重试逻辑由子类实现,如果允许执行重试逻辑if(!callResult.isSuccess() && canRetry()){doPush(deviceVO, content);}//4.trace结束span.finish()}//具体推送逻辑由子类实现protected abstract CallResult doPush(AppDeviceVO deviceDO, Object content) ;//是否允许重试由子类实现,有些通道之前没有做消息排重,所有不能重试protected abstract boolean canRetry(CallResult callResult);}class XingePushStrategy extends AbstractPushStrategy{@Overrideprotected CallResult doPush(AppDeviceVO deviceDO, Object content) {//执行推送逻辑}@Overrideprotected boolean canRetry(CallResult callResult){return false}
}
总结
通过模板定义了流程,将通用逻辑放在父类实现,减少了重复代码,个性化逻辑由子类自己实现,子类间修改代码互不干扰也不会破坏流程。
观察者设计模式
观察者设计模式定义
顾名思义,此模式需要有观察者(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 的事件机制发布“支付成功事件”,这样负责累加消费次数和负责语音播报的订阅者就会收到“支付成功事件”,进而做各自的业务逻辑,画个简单的图描述一下:
代码结构大体如下:
/**
支付回调处理者
*/
class PayCallBackController implements ApplicationContextAware {private ApplicationContext applicationContext;//如果想获取applicationContext需要实现ApplicationContextAware接口,Spring容器会回调setApplicationContext方法将applicationContext注入进来@Overridepublic void setApplicationContext(ApplicationContext applicationContext)throws BeansException {this.applicationContext = applicationContext;}@RequestMapping(value = "/pay/callback.do")public View callback(HttpServletRequest request){if(paySuccess(request){//构造支付成功事件PaySuccessEvent event = buildPaySuccessEvent(...);//通过applicationContext发布事件,从而达到通知观察者的目的this.applicationContext.publishEvent(event);}}
}
/*** 语音播报处理者**/
public class VoiceBroadcastHandler implements ApplicationListener<PaySuccessEvent>{@Overridepublic void onApplicationEvent(PaySuccessEvent event) {//语音播报逻辑}
}//其他处理者的逻辑类似
总结
观察者模式将被观察者和观察者之间做了解耦,观察者存在与否不会影响被观察者的现有逻辑。
装饰器设计模式
装饰器设计模式定义
装饰器用来包装原有的类,在对使用者透明的情况下做功能的增强,比如 java 中的 BufferedInputStream 可以对其包装的 InputStream 做增强,从而提供缓冲功能。
使用场景
希望对原有类的功能做增强,但又不希望增加过多子类时,可以使用装饰器模式来达到同样的效果。
实践经验
笔者之前在推动整个公司接入 trace 体系,因此也提供了一些工具来解决 trace 的自动织入和上下文的自动传递,为了支持线程间的上下文传递,我增加了TraceRunnableWrapper
这个装饰类,从而起到将父线程的上下文透传到子线程中,对使用者完全透明,代码如下:
/**
可以自动携带trace上下文的Runnable装饰器
*/
public class TraceRunnableWrapper implements Runnable{//被包装的目标对象private Runnable task;private Span parentSpan = null;public TraceRunnableWrapper(Runnable task) {//1.获取当前线程的上下文(因为new的时候还没有发生线程切换,所以需要在这里将上下文获取)//对这块代码感兴趣的可以查看opentracing APIio.opentracing.Scope currentScope = GlobalTracer.get().scopeManager().active();//2.保存父上下文parentSpan = currentScope.span();this.task = task;}@Overridepublic void run() {//run的时候将父线程的上下文绑定到当前线程io.opentracing.Scope scope = GlobalTracer.get().scopeManager().activate(parentSpan,false);task.run();}
}//使用者
new Thread(new Runnable(){run(...)}).start()替换为new TraceRunnableWrapper(new Runnable(){run(...)}).start()
总结
使用装饰器模式做了功能的增强,对使用者来说只需要做简单的组合就能继续使用原功能。
外观设计模式
外观设计模式定义
何为外观,就是对外提供一个统一的入口,一是可以影藏系统内部的细节,二是可以降低使用者的复杂度,比如 SpringMvc 中的 DispaterServlet,所有的 Controller 都是通过 DispaterServlet 统一暴露。
使用场景
降低使用者的复杂度,简化客户端的接入成本。
实践经验
笔者所在的公司对外提供了一些开放能力给第三方 ISV,比如设备管控、统一支付、对账单下载等能力,由于分属于不同的团队,所以对外提供的接口形式各异,初期还好,接口不多,ISV 也能接受,但是后期接口多了 ISV 就开始抱怨接入成本太高,为了解决这一问题,我们在开放接口前面加了一层前端控制器 GatewayController,其实就是我们后来开放平台的雏形,GatewayController 对外统一暴露一个接口 gateway.do,将对外接口的请求参数和响应参数统一在 GatewayController 做收敛,GatewayController 往后端服务路由时也采用统一接口,改造前后对比如下图:
大概代码如下:
使用者:
HttpClient.doPost("/gateway.do","{'method':'trade.create','sign':'wxxaaa','timestamp':'15311111111'},'bizContent':'业务参数'")GatewayController:
@RequestMapping("/gateway.do")
JSON gateway(HttpServletRequest req){//1.组装开放请求OpenRequest openRequest = buildOpenRequest(req);OpenResponse openResponse = null;//2.请求路由if("trade.create".equals(openRequest.getMethod()){//proxy to trade service by dubboopenResponse = TradeFacade.execute(genericParam);} else if("iot.message.push".equals(openRequest.getMethod()){//proxy to iot service by httpclientopenResponse = HttpClient.doPost('http://iot.service/generic/execute'genericParam);}if(openResponse.isSuccess()){return {"code":"10000","bizContent":openResponse.getResult()};}else{return {"code":"20000","bizCode":openResponse.getCode()};}}
总结
采用外观模式屏蔽了系统内部的一些细节,降低了使用者的接入成本,就拿GatewayController
来说,ISV 的鉴权,接口的验签等重复工作统一由它实现,ISV 对接不同的接口只需要关心一套接口协议接口,由GatewayController
这一层做了收敛。
设计模式在我工作中的巧妙实践相关推荐
- 大家一起来说一说自己掌握的设计模式和在工作中的应用场景,分享经验
做了比较多的通讯和SDK,虽然是用C++,但是SDK对外的都是C的接口,而且项目中主要是消息封装,socket IO模型和多线程, 消息收发,业务比较少,所以用的OOP也比较少,用了很多年C++,却一 ...
- 收藏!工作中Git使用实践和常用命令流程合集
来自:匠心Java 工作中git是一项必不可少的技能,在项目的开发进程中起着至关重要的作用 下面介绍一些git在工作中的一些使用实践.常用流程.常用命令,供大家参考! 一:前言 Git的定义是:分布式 ...
- 结构型设计模式在公司项目中的运用实践
关于设计模式的一些实战总结 -- 常见的结构型设计模式 在设计模式里面,有一种叫做适配器的设计模式 Adapter Design Pattern ,这类适配器模式通常应用于做不同接口之间的适配和调整, ...
- 实战!工作中常用到哪些设计模式
前言 平时我们写代码呢,多数情况都是流水线式写代码,基本就可以实现业务逻辑了.如何在写代码中找到乐趣呢,我觉得,最好的方式就是:使用设计模式优化自己的业务代码.今天跟大家聊聊日常工作中,我都使用过哪些 ...
- [vue] 实际工作中,你总结的vue最佳实践有哪些?
[vue] 实际工作中,你总结的vue最佳实践有哪些? .babelrc 是目前 babel-polyfill 的最佳实践 { "presets": [ [ "@babe ...
- 工作中常用的 6 种设计模式!
前言 哈喽,大家好. 平时我们写代码呢,多数情况都是流水线式写代码,基本就可以实现业务逻辑了.如何在写代码中找到乐趣呢,我觉得,最好的方式就是:使用设计模式优化自己的业务代码.今天跟大家聊聊日常工作中 ...
- git add 所有修改文件_Git 技术干货!工作中quot;Gitquot;的使用实践和常用命令合集!
作者:洋仔聊编程 出自:InfoQ 写作平台 原文:xie.infoq.cn/article/1ce91dc60431b1b8845729d41 工作中git是一项必不可少的技能,在项目的开发进程中起 ...
- 工作中的设计模式 —— 策略模式
前言 返利网站 https://m.cpa5.cn/ 策略模式是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换. 使用场景 策略模式在工作中使用的 ...
- 谈谈工作中常用的设计模式
今天跟大家聊聊开发过程中用到的几种设计模式.包括在校招,社招面试的时候都要问到的一些设计模式. 先看文章,如果还是不怎么理解跟明白的话,你可以回头看看这个视频讲解:设计模式详解 面向对象的实现 设计模 ...
最新文章
- 由树先序遍历和中序遍历输出其后续遍历
- mysql中的各种join整理
- vue摸板 大数据_Vue和DataV强强联合,这个大数据可视化模板你一定要拥有
- C语言不挂科之我爱谭浩强——选择填空拿满分(附例题答案和知识点详解)
- 2016年世界编程大赛_2016年热门编程趋势
- mysql数据库5.7配置文件_mysql数据库5.7版本部署
- 【ElasticSearch】如何使用 ElasticSearch 搜索单词的一部分 模糊搜索 正则匹配 前缀匹配
- C#程序员66个编码好习惯
- mybatis plus+spring boot 多租户动态数据源实现方案
- (原创) 学生宿舍管理系统(简易版) C语言
- 差异表达基因热图怎么看_基因表达谱热图绘制
- 机器人聊天软件c#_聊天机器人_c#应用
- Maven插件wagon-maven-plugin自动化部署Java项目到Linux远程服务器
- 华为5g服务器硬件供应商,华为P50系列硬件供应商名单曝光,国产化程度高,5G版或延后上市...
- Win10系统中VMware虚拟机启动蓝屏?
- MoCo中的InfoNCE
- 2023 java面试题整理
- 硬件电路-MP2451组成的电压反转/极性反转电路设计
- 自然语言处理与化学的关系
- @DateTimeFormat与@JsonFormat不完全解析
热门文章
- Cell综述-建立因果关系:合成菌群在植物菌群研究中的机会
- Nature:根系菌群参与磷胁迫和免疫的平衡
- pandas使用groupby函数、first函数、last函数分别获得每个分组的第一行和最后一行数据(first/last row of each group in dataframe)
- python使用matplotlib可视化使用subplots子图、subplots绘制子图并为可视化的子图添加主标题(subplots main title)
- plotly基于dataframe数据绘制股票自定义K线图
- Python可视化(matplotlib)在图像中添加文本和标记(Text and Annotation)
- 机器学习中过拟合、欠拟合与方差、偏差的关系是什么?
- Python之MySQL数据库增删改查操作
- android alpha不起作用,API 28(P)的Android设计支持库不起作用
- linux进程间通讯-有名管道