/ 今日科技快讯 /
8月25日,美图公司发布2021年中期业绩报告。截至6月30日,公司已购买的比特币和以太坊公允价值分别约为6520万美元、3220万美元。上半年比特币公允价值减少1.119亿元人民币,以太坊增加9490万元人民币,虚拟货币投资总计亏损1700万元人民币。
/ 作者简介 /
明天是周六啦,又到了休息的时候,我们下周再见!
本篇文章来自小呆呆666的投稿,结合业务详细地分析了两种设计模式,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
小呆呆666的博客地址:
https://juejin.cn/user/2840793776393847/posts
/ 前言 /
有段时间没写文章了,最近沉迷Rust,无法自拔,锈儿有毒;这真是门非常有趣的语言,很多地方的设计,真的是满足了我所有的向往。
当然,这也不是一门简单的语言,提出所有权的概念,引入了极多符号:mut、&mut、ref mut、&、*、as_mut、as_ref。。。让人头秃。。。
之前看到过一句话,觉得很不错:学习Rust并不会给你带来智商上的优越感,但或许会让你重新爱上编程
大家如果阅读过一些开源框架的源码,可能会发现其中数不尽的抽象类,设计模式拈手而来,在功能框架中,可以使用设计模式随心所欲的解耦;在实际的复杂业务中,当然也可以应用合适的设计模式。
这篇文章,我会结合较为常见的实际业务场景,探讨如何使用合适的设计模式将业务解耦
此处的应用绝不是生搬硬套,是我经过深思熟虑,并将较为复杂的业务进行全面重构后,得出的一套行之有效的思路历程
任何一个设计模式都是一个伟大的经验及其思想总结,千人千面,如果对文章中内容,有不同的意见,希望你能在评论中提出,我们共同探讨,共同进步
本文章是一篇弱代码类型文章,我会画大量的图片向大家展示,引用设计模式后,会对原有的业务流程,产生什么样的影响。
/ 前置知识 /
这里,需要了解下基础知识,什么是责任链模式和策略模式
责任链模式,在很多开源框架中都是有所应用,你如果听到啥啥拦截器,基本就是责任链模式,责任链模式的思想很简单,但是有很多种实现方式
最简单的链表实现就和OkHttp的拦截器实现大相径庭
OkHttp的拦截器实现和Dio拦截器实现结构相同,但遍历方式不一样
很多骚操作:我喜欢OkHttp的实现方式,喜欢dio的Api设计,结尾会给出一个结合这俩者思想的通用拦截器
策略模式,或是天生适合业务,同一模块不同类型业务,如果行为相同,或许就可以考虑使用策略模式去解耦了
责任链模式
这边用Dart写一个简单的拦截器,dart和java非常像
为了减少语言差异,我就不使用箭头语法了
下划线表示私有
用啥语言不重要,这边只是用代码简单演示下思想
此处实现就用链表了;如果,使用数组的形式,需要多写很多逻辑,数组的优化写法在结尾给出,此处暂且不表
结构
责任链的结构,通常有俩种结构
链表结构:链表构建责任链,十分便捷的就能和下一节点建立联系
数组结构:数组,用通用的List即可,方便增删,不固定长度(别费劲的用固定长度Array了,例如:int[]、String[])
实现一个链表实体很简单
abstract class InterceptChain<T> {InterceptChain? next;void intercept(T data) {next?.intercept(data);}
}
实现
拦截器实现
/// 该拦截器以最简单的链表实现
abstract class InterceptChain<T> {InterceptChain? next;void intercept(T data) {next?.intercept(data);}
}class InterceptChainHandler<T> {InterceptChain? _interceptFirst;void add(InterceptChain interceptChain) {if (_interceptFirst == null) {_interceptFirst = interceptChain;return;}var node = _interceptFirst!;while (true) {if (node.next == null) {node.next = interceptChain;break;}node = node.next!;}}void intercept(T data) {_interceptFirst?.intercept(data);}
}
使用
调整add顺序,就调整了对应逻辑的节点,在整个责任链中的顺序
去掉intercept重写方法中的super.intercept(data),就能实现拦截后续节点逻辑
void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(OneIntercept());intercepts.add(TwoIntercept());intercepts.intercept("测试拦截器");
}class OneIntercept extends InterceptChain<String> {@overridevoid intercept(String data) {data = "$data:OneIntercept";print(data);super.intercept(data);}
}class TwoIntercept extends InterceptChain<String> {@overridevoid intercept(String data) {data = "$data:TwoIntercept";print(data);super.intercept(data);}
}
打印结果
测试拦截器:OneIntercept
测试拦截器:OneIntercept:TwoIntercept
策略模式
结构
策略模式最重要的:应该就是对抽象类的设计,对行为的抽象
实现
定义抽象类,抽象行为
/// 结合适配器模式的接口适配:抽象必须实现行为,和可选实现行为
abstract class BusinessAction {///创建相应资源:该行为必须实现void create();///可选实现void dealIO() {}///可选实现void dealNet() {}///可选实现void dealSystem() {}///释放资源:该行为必须实现void dispose();
}
实现策略类
//Net策略
class NetStrategy extends BusinessAction {@overridevoid create() {print("创建Net资源");}@overridevoid dealNet() {print("处理Net逻辑");}@overridevoid dispose() {print("释放Net资源");}
}///IO策略
class IOStrategy extends BusinessAction {@overridevoid create() {print("创建IO资源");}@overridevoid dealIO() {print("处理IO逻辑");}@overridevoid dispose() {print("释放IO资源");}
}
使用
void main() {var type = 1;BusinessAction strategy;//不同业务使用不同策略if (type == 0) {strategy = NetStrategy();} else {strategy = IOStrategy();}//开始创建资源strategy.create();//......... 省略N多逻辑(其中某些场景,会有用到Net业务,和上面type是关联的)//IO业务:开始处理业务strategy.dealIO();//......... 省略N多逻辑//释放资源strategy.dispose();
}
结果
创建IO资源
处理IO逻辑
释放IO资源
/ 适合的业务场景 /
这边举一些适合上述设计模式的业务场景,这些场景是真实存在的!
这些真实的业务,使用设计模式解耦和纯靠if else怼,完全是俩种体验!
代码如诗,这并不是一句玩笑话。
连环弹窗业务
业务描述
连环弹窗夺命call来袭。。。
好家伙,套娃真是无所不在,真不是我们代码套娃,实在是业务套娃,手动滑稽.png
图示弹窗业务(连环弹窗业务1)
直接开搞
看到这个业务,大家会去怎么做呢?
有人可能会想,这么简单的业务还需要想吗?直接写啊!
A:在确定回调里面,跳转B弹窗
B:查看详情按钮跳转C弹窗
。。。
好一通套后,终于写完了
产品来了,加需求:B和C弹窗之间要加个预览G弹窗,点击B的查看详情按钮,跳转预览G弹窗;预览G弹窗只有一个确定按钮,点击后跳转C弹窗
你心里可能要想了,这特么不是坑爹?
业务本来就超吉尔套,我B弹窗里面写的跳转代码要改,传参要改,而且还要加弹窗!
先要去找产品撕比,撕完后
然后继续在屎山上,小心翼翼的再拉了坨shit
这座克苏鲁山初成规模
连环弹窗业务2
产品又来了,第一稿需求不合理,需要调整需求
交换C和D弹窗位置,D弹窗点击下一步的时候,需要加一个校验请求,通过后才能跳转到C弹窗
你眉头一皱,发现事情没有表面这么简单
由于初期图简单,几乎都写在一个文件里,眼花缭乱弹窗回调太多,而且弹窗样式也不一样
现在改整个流程,导致你整个人脑子嗡嗡响
心中怒气翻涌,找到产品说
回来,坐在椅子上,心里想:
老夫写的代码天衣无缝,这什么几把需求
可恶,这次测试,起码要给我多提十几个BUG
克苏鲁山开始狰狞(连环弹窗业务3)
产品飘来,加改需求:如此,如此,,,这般,这般,,,
产品:改下,,,然后,扔给你几十页的PRD
你看了看这改了几十版的克苏鲁山,这几十个弹窗逻辑居然都写在一个文件里,快一万行的代码。。。
心里不禁想:
本帅比写的代码果然牛批,或许这就是艺术!艺术总是曲高和寡,难被人理解!而我的代码更牛批,连我自己都看不懂了!
这代码行数!这代码结构!不得拍个照留念下,传给以后的孩子当传家宝供着!
心里不禁嘚瑟:
这块业务,除了我,还有谁敢动,成为头儿的心腹,指日可待!
但,转念深思后:事了拂衣去,深藏功与名
重构
随着业务的逐渐复杂,最初的设计缺点会逐渐暴露;重构有缺陷的代码流程,变得势在必行,这会极大的降低维护成本
如果心中对责任链模式有一些概念的话,会发现上面的业务,极其适合责任链模式!
对上面的业务进行分析,可以明确一些事
这个业务是一个链式的,有着明确的方向性:单向,从头到尾指向
业务拆分开,可以将一个弹窗作为单颗粒度,一个弹窗作为节点
上级的业务节点可以对下级节点拦截(点击取消,拒绝按钮,不再进行后续业务)
重构上面的代码,只要明确思想和流程就行了
第一稿业务 :责任链
代码:简写
void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(AIntercept());intercepts.add(BIntercept());intercepts.add(CIntercept());intercepts.add(DIntercept());intercepts.add(EIntercept());intercepts.add(FIntercept());intercepts.intercept("测试拦截器");
}
第二稿业务:责任链
代码:简写
void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(AIntercept());intercepts.add(BIntercept());intercepts.add(GIntercept());intercepts.add(CIntercept());intercepts.add(DIntercept());intercepts.add(EIntercept());intercepts.add(FIntercept());intercepts.intercept("测试拦截器");
}
第三稿业务:责任链
代码:简写
void main() {var intercepts = InterceptChainHandler<String>();intercepts.add(AIntercept());intercepts.add(BIntercept());intercepts.add(GIntercept());intercepts.add(DIntercept());intercepts.add(CIntercept());intercepts.add(EIntercept());intercepts.add(FIntercept());intercepts.intercept("测试拦截器");
}
总结:经过责任链模式重构后,业务节点被明确的区分开,整个流程从代码上看,都相当的清楚,维护将变的异常轻松;或许,此时能感受到一些,编程的乐趣了
花样弹窗业务
业务描述
来描述一个新的业务:这个业务场景真实存在某办公软件
进入APP首页后,和后台建立一个长连接
后台某些工单处理后,会通知APP处理,此时app会弹出处理工单的弹窗(app顶部)
弹窗类型很多:工单处理弹窗,流程审批弹窗,邀请类型弹窗,查看工单详情弹窗,提交信息弹窗。。。
弹窗弹出类型,是根据后台给的Type进行判断:从而弹出不同类型弹窗、点击其按钮,跳转不同业务,传递不同参数。
分析
1. 确定设计
这个业务,是一种渐变性的引导你搭建克苏鲁代码山
在前期开发的时候,一般只有俩三种类型弹窗,前期十分好做;根本不用考虑如何设计,抬手一行代码,反手一行代码,就能搞定
但是后来整个业务会渐渐的鬼畜,不同类型会慢慢加到几十种之多!!!
首先这个业务,使用责任链模式,肯定是不合适的,因为弹窗之间的耦合性很低,并没有什么明确的上下游关系
但是,这个业务使用策略模式非常的合适!
type明确:不同类型弹出不同弹窗,按钮执行不同逻辑
抽象行为明确:一个按钮就是一种行为,不同行为的实现逻辑大相径庭
2. 抽象行为
多样弹窗的行为抽象,对应其按钮就行了
确定、取消、同意、拒绝、查看详情、我知道了、提交
直接画图来表示吧
实现
来看下简要的代码实现,代码不重要,重要的是思想,这边简要的看下代码实现流程
抽象基类
/// 默认实现抛异常,可提醒未实现方法被误用
abstract class DialogAction {///确定void onConfirm() {throw 'DialogAction:not implement onConfirm()';}///取消void onCancel() {throw 'DialogAction:not implement onCancel()';}///同意void onAgree() {throw 'DialogAction:not implement onAgree()';}///拒绝void onRefuse() {throw 'DialogAction:not implement onRefuse()';}///查看详情void onDetail() {throw 'DialogAction:not implement onDetail()';}///我知道了void onKnow() {throw 'DialogAction:not implement onKnow()';}///提交void onSubmit() {throw 'DialogAction:not implement onSubmit()';}
}
实现逻辑类
class OneStrategy extends DialogAction {@overridevoid onConfirm() {print("确定");}@overridevoid onCancel() {print("取消");}
}class TwoStrategy extends DialogAction{@overridevoid onAgree() {print("同意");}@overridevoid onRefuse() {print("拒绝");}
}//........省略其他实现
使用
void main() {//根据接口获取var type = 1;DialogAction strategy;switch (type) {case 0:strategy = DefaultStrategy();break;case 1:strategy = OneStrategy();break;case 2:strategy = TwoStrategy();break;case 3:strategy = ThreeStrategy();break;case 4:strategy = FourStrategy();break;case 5:strategy = FiveStrategy();break;default:strategy = DefaultStrategy();break;}//聚合弹窗按钮触发事件(不同弹窗的确定按钮,皆可聚合为一个onConfirm事件,其它同理)BusinessDialog(//通过传入的type,显示对应类型的弹窗type: type,//确定按钮onConfirm: () {strategy.onConfirm();},//取消按钮onCancel: () {strategy.onCancel();},//同意按钮onAgree: () {strategy.onAgree();},//拒绝按钮onRefuse: () {strategy.onRefuse();},//查看详情按钮onDetail: () {strategy.onDetail();},//我知道了按钮onKnow: () {strategy.onKnow();},//提交按钮onSubmit: () {strategy.onSubmit();},);
}
/ 复杂业务场景演变 /
我们看下,一个简单的提交业务流,怎么逐渐变的狰狞
我会逐渐给出一个合适的解决方案,如果大家有更好的想法,务必在评论区告诉鄙人
业务描述:我们的车子因不可抗原因坏了,要去维修厂修车,工作人员开始登记这个损坏车辆。。。
业务的演变
第一稿:初始业务
登记一个维修车辆的流程,实际上还是满麻烦的
登记一个新车,需要将车辆详细信息登记清楚:车牌、车架、车型号、车辆类型、进出场时间、油量、里程。。。
还需要登记一下用户信息:姓名、手机号、是否隶属公司。。。
登记车损程度:车顶、车底、方向盘、玻璃、离合器、刹车。。。
车内物品:车座皮套、工具。。。
以及其他我没想到的。。。
最后:提交所有登记好的信息
第一稿,业务流程十分清晰,细节复杂,但是做起来不难
第二稿(实际是多稿聚合):增加下述几个流程
外部登记:外部登记了一个维修车辆部分信息(后台,微信小程序,H5等等),需要在app上完善信息,提交接口不同(必带车牌号)
快捷洗车:洗车业务极其常见,快捷生成对应信息,提交接口不同
预约订单登记:预约好了车辆一部分一些信息,可快捷登记,提交接口不同(必带车牌号)
因为登记维修车辆流程,登记车辆信息流程极其细致繁琐,我们决定复用登记新车模块
因为此处逻辑大多涉及开头和结尾,中间登记车辆信息操作几乎未改动,复用想法是可行的
如果增加车辆登记项,新的三个流程也必须提交这些信息;所以,复用势在必行
因为这一稿需求,业务也变得愈加复杂
第三稿
现在要针对不同的车辆类型,做不同的处理;车类型分:个人车,集团车
不同类型的登记,在提交的时候,需要校验不同的信息;校验不通过,需要提示用户,并且不能进行提交流程
提交后,需要处理下通用业务,然后跳转到某个页面
第三稿的描述不多,但是,大大的增加了复杂度
尤其是不同类型校验过程还不同,还能中断后续提交流程
提交流程后,还需要跳转通用页面
开发探讨
第一稿
正常流程开发、、、
第二稿
对于第二稿业务,可以好好考虑下,怎么去设计?
开头和结尾需要单独写判断,去处理不同流程的业务,这至少要写俩个大的判断模块,接受数据的入口模块可能还要写判断
这样就非常适合策略模式去做了
开头根据执行的流程,选择相应的策略对象,后续将逻辑块替换抽象的策略方法就OK了,大致流程如下
第三稿
第三稿的需求,实际上,已经比较复杂了
整个流程中掺杂着不同业务流程处理,不同流程逻辑又拥有阻断下游机制(绿色模块)
下游逻辑又会合流(结尾)的多种变换
在这一稿的需求
使用策略模式肯定是可以的
阻断那块(绿色模块)需要单独处理下:抽象方法应该拥有返回值,外层根据返回值,判断是否进行后续流程
但!这!也太不优雅了!
思考上面业务一些特性
拦截下游机制
上游到下游、方向明确
随时可能插入新的业务流程。。。
可以用责任链模式!但,需要做一些小改动!这地方,我们可以将频繁变动的模块用责任链模式全都隔离出来
看下,使用责任链模式改造后流程图
浏览上述流程图可发现,本来是极度杂乱糅合的业务,可以被设计相对更加平行的结构
对于上述流程,可以进一步分析,并进一步简化:对整体业务分析,我们需要去关注其变或不变的部分
不变:整体业务变动很小的是,登记信息流程(主体逻辑这块),此处的相关变动是很小的,对所有流程也是共用的部分
变:可以发现,开头和结尾是变动更加频繁的部分,我们可以对此处逻辑进行整体的抽象
抽象多变的开头和结尾
所以我们抽象拦截类,可以做一些调整
abstract class InterceptChainTwice<T> {InterceptChainTwice? next;void onInit(T data) {next?.onInit(data);}void onSubmit(T data) {next?.onSubmit(data);}
}
来看下简要的代码实现,代码不重要,主要看看实现流程和思想
抽象拦截器
abstract class InterceptChainTwice<T> {InterceptChainTwice? next;void onInit(T data) {next?.onInit(data);}void onSubmit(T data) {next?.onSubmit(data);}
}class InterceptChainTwiceHandler<T> {InterceptChainTwice? _interceptFirst;void add(InterceptChainTwice interceptChain) {if (_interceptFirst == null) {_interceptFirst = interceptChain;return;}var node = _interceptFirst!;while (true) {if (node.next == null) {node.next = interceptChain;break;}node = node.next!;}}void onInit(T data) {_interceptFirst?.onInit(data);}void onSubmit(T data) {_interceptFirst?.onSubmit(data);}
}
实现拦截器
/// 开头通用拦截器
class CommonIntercept extends InterceptChainTwice<String> {@overridevoid onInit(String data) {//如果有车牌,请求接口,获取数据//.................//填充页面super.onInit(data);}
}/// 登记新车拦截器
class RegisterNewIntercept extends InterceptChainTwice<String> {@overridevoid onInit(String data) {//处理开头针对登记新车的单独逻辑super.onInit(data);}@overridevoid onSubmit(String data) {var isPass = false;//如果校验不过,拦截下游逻辑if (!isPass) {return;}// ......super.onSubmit(data);}
}/// 省略其他实现
使用
void main() {var type = 0;var intercepts = InterceptChainTwiceHandler();intercepts.add(CommonIntercept());intercepts.add(CarTypeDealIntercept());if (type == 0) {//登记新车intercepts.add(RegisterNewCarIntercept());} else if (type == 1) {//外部登记intercepts.add(OutRegisterIntercept());} else if (type == 2) {//快捷洗车intercepts.add(FastWashIntercept());} else {//预约订单登记intercepts.add(OrderRegisterIntercept());}intercepts.add(TailIntercept());//业务开始intercepts.onInit("传入数据源");//开始处理N多逻辑//............................................................//经历了N多逻辑//提交按钮触发事件SubmitBtn(//提交按钮onSubmit: () {intercepts.onSubmit("传入提交数据");},);
}
总结
关于代码部分,关键的代码,我都写出来,用心看看,肯定能明白我写的意思
也不用找我要完整代码了,这些业务demo代码写完后,就删了
本栏目这个业务,实际上是非常常见的的一个业务,一个提交流程与很多其它的流程耦合,整个业务就会慢慢的变的鬼畜,充满各种判断,很容易让人陷入泥泞,或许,此时可以对已有业务进行思考,如何进行合理的优化
该业务的演变历程,和开发改造是本人的一次思路历程,如大家有更好的思路,还请不吝赐教。
/ 通用拦截器 /
我结合OkHttp的思想和Dio的API,封装了俩个通用拦截器,这边贴下代码,如果哪里有什么不足,请及时告知本人
说明下:这是Dart版本的
抽象单方法
///一层通用拦截器,T的类型必须一致
abstract class InterceptSingle<T> {void intercept(T data, SingleHandler handler) => handler.next(data);
}///添加拦截器,触发拦截器方法入口
class InterceptSingleHandler<T> {_InterceptSingleHandler _handler = _InterceptSingleHandler(index: 0,intercepts: [],);void add(InterceptSingle intercept) {//一种类型的拦截器只能添加一次for (var item in _handler.intercepts) {if (item.runtimeType == intercept.runtimeType) {return;}}_handler.intercepts.add(intercept);}void delete(InterceptSingle intercept) {_handler.intercepts.remove(intercept);}void intercept(T data) {_handler.next(data);}
}///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
abstract class SingleHandler {next(dynamic data);
}///实现init处理器
class _InterceptSingleHandler extends SingleHandler {List<InterceptSingle> intercepts;int index;_InterceptSingleHandler({required this.index,required this.intercepts,});@overridenext(dynamic data) {if (index >= intercepts.length) {return;}var intercept = intercepts[index];var handler =_InterceptSingleHandler(index: index + 1, intercepts: intercepts);intercept.intercept(data, handler);}
}
抽象双方法
///俩层通用拦截器,T的类型必须一致
abstract class InterceptTwice<T> {void onInit(T data, TwiceHandler handler) => handler.next(data);void onSubmit(T data, TwiceHandler handler) => handler.next(data);
}///添加拦截器,触发拦截器方法入口
class InterceptTwiceHandler<T> {_TwiceInitHandler _init = _TwiceInitHandler(index: 0, intercepts: []);_TwiceSubmitHandler _submit = _TwiceSubmitHandler(index: 0, intercepts: []);void add(InterceptTwice intercept) {//一种类型的拦截器只能添加一次for (var item in _init.intercepts) {if (item.runtimeType == intercept.runtimeType) {return;}}_init.intercepts.add(intercept);_submit.intercepts.add(intercept);}void delete(InterceptTwice intercept) {_init.intercepts.remove(intercept);_submit.intercepts.remove(intercept);}void onInit(T data) {_init.next(data);}void onSubmit(T data) {_submit.next(data);}
}///------------实现不同处理器 参照 dio api设计 和 OkHttp实现思想---------------
abstract class TwiceHandler {next(dynamic data);
}///实现init处理器
class _TwiceInitHandler extends TwiceHandler {List<InterceptTwice> intercepts;int index;_TwiceInitHandler({required this.index,required this.intercepts,});@overridenext(dynamic data) {if (index >= intercepts.length) {return;}var intercept = intercepts[index];var handler = _TwiceInitHandler(index: index + 1, intercepts: intercepts);intercept.onInit(data, handler);}
}///实现submit处理器
class _TwiceSubmitHandler extends TwiceHandler {List<InterceptTwice> intercepts;int index;_TwiceSubmitHandler({required this.index,required this.intercepts,});@overridenext(dynamic data) {if (index >= intercepts.length) {return;}var intercept = intercepts[index];var handler = _TwiceSubmitHandler(index: index + 1, intercepts: intercepts);intercept.onSubmit(data, handler);}
}
推荐阅读:
我的新书,《第一行代码 第3版》已出版!
使用OpenGL挑战抖音蓝线特效
PermissionX 1.5发布,支持申请Android特殊权限啦
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注