当前位置: 首页 > 科技观察

我写的代码又被CTO骂了,

时间:2023-03-16 10:33:14 科技观察

大部分时间我写一些业务代码,也许一堆CRUD就能解决问题,但是这种工作对技术人提升不多,怎么办让自己从业务中解脱出来,找到编写代码的乐趣?我做了一些尝试,使用设计模式来改进我的业务代码就是其中之一。图片来自Pexels《你的代码写的一塌糊涂》,今天我的代码被CTO当成典型骂了。。。所以他给了我如下建议:ChainofresponsibilitydesignpatternpatterndefinitionrequestinaOn-链式处理,链上的acceptor完成处理后,决定是继续传递还是中断当前的处理流程。适用场景适合多节点进程处理。每个节点完成自己负责的部分,节点之间不知道对方的存在,比如OA的审批流程,JavaWeb开发中的过滤机制。举个生活中的例子,笔者在租房时遇到了一个所谓的黑中介。租房的时候觉得自己像个神,坏了的东西让他修的时候就觉得自己像个孙子。中介让我找店家客服,店家客服让我找房东,房东又让我找她老公,最后终于搞定了(一定要找正规的中介租房)。实战经验笔者目前做的业务是校园团餐的聚合支付。业务流程非常简单:学生打开手机支付码进行支付。食堂阿姨用机器扫支付码收款。有大学食堂的背景。食堂是有补贴的,饭菜比较便宜,所以学校不太愿意让市民到学校食堂消费。鉴于此,我们在支付前增加了一套是否允许支付的验证逻辑。大致是这样的:某个档口只允许某类用户消费。比如教师档只允许教师消费,学生档不允许校外用户消费。某个档口每天只允许某类用户消费几次,比如教师食堂每天只允许学生消费一次。是否允许非清真学生消费,比如一些清真餐厅,非清真学生是不允许消费的。针对这几种情况,我建立了三类过滤器,分别是:SpecificCardUserConsumeLimitFilter:根据用户类型判断是否允许消费。DayConsumeTimesConsumeLimitFilter:根据每日消费次数判断是否允许消费。MuslimConsumeLimitFilter:是否允许非清真用户消费。判断逻辑是先通过SpecificCardUserConsumeLimitFilter判断当前用户是否可以在这个档位消费。如果允许,则继续使用DayConsumeTimesConsumeLimitFilter判断当天的消费次数是否已经用完;如果不满足,继续使用MuslimConsumeLimitFilter判断当前用户是否满足清真餐厅就餐条件。如果前三个判断有一个不满足,则提前返回。部分代码如下:publicbooleancanConsume(Stringguid,StringshopId,StringsupplierId){//获取用户信息,用户信息包括类型(student:学生,teacher:老师,unknown:未知用户),种族(han:汉族,mg:Mongolian)UserInfouserInfo=getUserInfo(uid);//获取消费限额信息,限额信息包括是否允许非清真消费,是否允许每类用户消费,允许消费的数量ConsumeConfigInfoconsumeConfigInfo=getConsumeConfigInfo(shopId,supplierId)//构造消费限额过滤器器链条ConsumeLimitFilterChainfilterChain=newConsumeLimitFilterChain();filterChain.addFilter(newSpecificCardUserConsumeLimitFilter());filterChain.addFilter(newDayConsumeTimesConsumeLimitFilter());filterChain.addFilter(newMuslimConsumeLimitFilterindo,filterindoColeboCheck());schoolMemberInfo,consumeConfigInfo);//filterChain.doFilter方法publicbooleandoFilter(ConsumeLimitFilterChainfilterChain,UserInfouserInfo,ConsumeConfigInfoconsumeConfigInfo){//迭代调用filterif(index=cardConsumeConfig.getDayConsumeTimesLimit()){returnfalse;}//其他情况继续通过returnfilterChain.doFilter(filterChain,memberInfo,consumeConfig);}总结:每个限制条件的判断逻辑封装在一个specificFilter中,如果修改了某个限制条件的逻辑,不会影响其他条件。如果需要添加新的限制,只需要重建一个Filter可以编织到FilterChain中,定义一系列算法,封装每一个算法,使它们可以互换。适用场景主要是剔除大量的ifelse代码,将每一个判断背后的算法逻辑提取到具体的policy对象中。当修改算法逻辑时,用户不会察觉,只需要修改策略对象的内部逻辑即可。这样的策略对象一般都实现了一个通用的接口,可以达到互通的目的。实战经验笔者之前有一个需求,用户扫码支付后,会向摊位收银台推送一条支付信息。.但由于历史原因,部分设备接入不同的推送平台。A类设备首先使用PigeonPush。如果失败,他们需要降级到长轮询机制。B类设备可直接使用自研推送平台。另一个现状是A类和B类的报文格式不同(不同团队开发,后期整合)。鉴于此,我抽象出PushStrategy接口,其具体实现包括IotPushStrategy和XingePushStrategy,分别对应自研推送平台的推送策略和信鸽平台的推送策略。用户可以针对不同的设备类型使用不同的推送策略。部分代码如下:/***推送策略*/publicinterfacePushStrategy{/**@paramdeviceVO设备对象,包括按钮设备sn,信鸽pushid@paramcontent,推送内容,一般为json*/publicCallResultpush(AppDeviceVOdeviceVO,Objectcontent);}IotPushStrategyimplementsPushStrategy{/**@paramdeviceVO设备对象,封装按键设备sn,信鸽pushid@paramcontent,推送内容,一般为json*/publicCallResultpush(AppDeviceVOdeviceVO,Objectcontent){//创建自研推送需要的推送消息platformMessagemessage=createPushMsg(deviceVO,content);//调用推送平台推送接口IotMessageService.pushMsg(message);}}XingePushStrategyimplementsPushStrategy{/**@paramdeviceVO设备对象,包括扣设备sn、信鸽pushid@paramcontent、推送内容,一般为json*/publicCallResultpush(AppDeviceVOdeviceVO,Objectcontent){//创建推送消息JSONObjectjsonObject=createPushMsg(content);//调用推送接口推送平台的if(!XinggePush.pushMsg(message)){//降级为长轮询...}}}/**消息推送服务*/MessagePushService{pushMsg(AppDeviceVOdeviceVO,Objectcontent){if(Adevice){XingePushStrategy.push(deviceVO,content);}elseif(Bdevice){IotPushStrategy.push(deviceVO,content);}}}总结:将各个通道的推送逻辑封装成具体的策略。某个策略的改变不会影响其他策略。由于实现了公共接口,策略可以相互替换,对用户友好,比如JavaThreadPoolExecutor中的任务拒绝策略,当线程池饱和时,会执行拒绝策略,具体的拒绝逻辑封装在RejectedExecutionHandler的rejectedExecution中。模板设计模式模式定义模板的价值在于骨架的定义。定义了骨架内处理问题的过程。通用处理逻辑一般由父类实现,个性化处理逻辑由子类实现。比如炒土豆丝和炸麻婆豆腐,大体逻辑是:剁菜下油炒菜,1、2、4类似,只是第三步不同。炒土豆丝要用铲子爆炒,而炒麻婆豆腐要用勺子轻轻推,不然豆腐会烂掉(疫情期间宅在家里学了很多菜)。使用不同场景的处理流程,部分逻辑是通用的,可以在父类中作为通用实现,而部分逻辑是个性化的,需要子类单独实现。实际体验还是和之前语音播报的例子一样。后来我们新增了两个需求:消息推送需要添加trace。部分频道推送失败需要重试。所以现在流程变成了这样:trace开始。频道开始推送。是否允许重试,如果允许则执行重试逻辑。跟踪结束。其中1和4是通用的,2和3是个性化的。鉴于此,我在具体推送策略之前加了一层父类策略,将通用逻辑放在父类中。修改后的代码如下:abstractclassAbstractPushStrategyimplementsPushStrategy{@OverridepublicCallResultpush(AppDeviceVOdeviceVO,Objectcontent){//1。构造spanSpanspan=buildSpan();//2.具体的频道推送逻辑由子类实现CallResultcallResult=doPush(deviceVO,content);//3.是否允许子类实现重试逻辑,如果允许则执行重试逻辑if(!callResult.isSuccess()&&canRetry()){doPush(deviceVO,content);}//4.traceendsspan.finish()}//具体的推送逻辑由子类实现protectedabstractCallResultdoPush(AppDeviceVOdeviceDO,Objectcontent);//是否重试允许由子类实现。部分通道之前没有做过消息去重,所以无法重试通过模板定义,通用逻辑放在父类减少重复代码,个性化逻辑由子类自己实现,子类之间修改代码,互不干扰,不打乱流程。观察者设计模式模式定义顾名思义,这个模式需要两个角色:观察者(Observer)和被观察者(Observable)。当Observable状态发生变化时,会通知Observer,而Observer一般会实现一个通用的接口。比如java.util.Observer,当Observable需要通知Observer时,只要一一调用Observer的update方法即可,Observer的处理成功与否应该不会影响Observable的处理。使用场景一个对象(Observable)状态改变需要通知其他对象。Observer的存在不影响Observable的处理结果。Observer的增删改对Observable没有任何感知。比如Kafka的消息订阅中,Producer向Topic发送消息。至于是一个还是十个Consumer订阅了这个Topic,Producer不需要关注。实践经验在责任链设计模式部分,我通过三个过滤器解决了消费限额检查的问题,其中一个用于测试消费次数。我这里只是读取了用户的消费次数,那么消费次数的累加是怎么做到的呢?其实就是用观察者模式来做累加的。具体的,当交易系统收到支付成功回调后,会通过Spring的事件机制发出“支付成功事件”。这样,负责累计消费次数和语音播报的订阅者就会收到“支付成功事件”,然后再做自己的业务逻辑。画个简单的图来描述一下:代码结构大致如下:/**支付回调处理器*/PayCallBackControllerimplementsApplicationContextAware{privateApplicationContextapplicationContext;//如果要获取applicationContext,需要实现ApplicationContextAware接口,Spring容器会回调setApplicationContext方法注入applicationContext@OverridepublicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{this.applicationContext=applicationContext;}@RequestMapping(value="/pay/callback.do")publicViewcallback(HttpServletRequestrequest){if(paySuccess(request){//构造支付成功事件PaySuccessEventevent=buildPaySuccessEvent(...);//通过applicationContext发布事件,从而达到通知观察者的目的//语音广播逻辑}}//其他处理器逻辑类似总结:观察者模式将观察者与观察者解耦呃,观察者存在与否,不会影响被观察者存在的逻辑。装饰器设计模式定义了装饰器在对用户透明的情况下包装原始类并增强功能。例如Java中的BufferedInputStream可以增强它包装的InputStream来提供缓冲功能。当你想增强原有类的功能,又不想增加太多的子类时,可以使用装饰器模式来达到同样的效果。实践经验作者当时是在推动整个公司接入trace系统,所以也提供了一些工具来解决trace的自动编织和context的自动传递。为了支持线程间的上下文传递,我添加了装饰类TraceRunnableWrapper,从而将父线程的上下文透明传递给子线程,对用户是完全透明的。代码如下:/**可自动携带跟踪上下文的Runnable装饰器*/publicclassTraceRunnableWrapperimplementsRunnable{//被包装的目标对象privateRunnabletask;privateSpanparentSpan=null;publicTraceRunnableWrapper(Runnabletask){//1。获取当前线程的上下文(因为新的Thread切换还没有发生,所以这里需要获取上下文)//如果对这段代码感兴趣可以查看opentracingAPIio.opentracing.ScopecurrentScope=GlobalTracer.get().scopeManager().active();//2.保存父上下文parentSpan=currentScope.span();this.task=task;}@Overridepublicvoidrun(){//运行时,将父线程的上下文绑定到当前线程io.opentracing.Scopescope=GlobalTracer.get().scopeManager().activate(parentSpan,false);task.run();}}//usernewThread(newRunnable(){run(...)}).start()被newTraceRunnableWrapper(newRunnable(){run(...)}).start()总结:使用装饰器模式增强功能。对于用户来说,只需要进行简单的组合,就可以继续使用原有的功能。外观设计模式外观的定义是为外界提供一个统一的入口:第一,它可以隐藏系统的内部细节。其次,它可以降低用户的复杂性。比如SpringMVC中的DispaterServlet,所有的Controller都是通过DispaterServlet统一暴露出来的。使用场景降低了用户的复杂度,简化了客户端的接入成本。实战经验笔者所在的公司向第三方ISV开放了一些能力,比如设备管控、统一支付、报表下载能力等。由于属于不同的团队,对外提供的接口形式也不同。早期接口不多,ISV可以接受。但是后来接口太多了,ISV开始抱怨接入成本太高。为了解决这个问题,我们在开放接口前面加了一层前端控制器GatewayController,这其实就是我们后来开放平台的雏形。GatewayController对外统一暴露一个接口gateway.do,将对外接口的请求参数和响应参数汇聚在GatewayController中。GatewayController在路由到后端服务时也使用统一接口。改造前后对比如下:大致代码如下:User:HttpClient.doPost("/gateway.do","{'method':'trade.create','sign':'wxxaaa','timestamp':'15311111111'},'bizContent':'业务参数'")GatewayController:@RequestMapping("/gateway.do")JSONgateway(HttpServletRequestreq){//1.组装一个开放请求OpenRequestopenRequest=buildOpenRequest(req);OpenResponseopenResponse=null;//2.请求路由if("trade.create".equals(openRequest.getMethod()){//proxytotradeservicebydubboopenResponse=TradeFacade.execute(genericParam);}elseif("iot.message.push".equals(openRequest.getMethod()){//proxytoiotservicebyhttpclientopenResponse=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层进行汇聚。作者:踩刀诗人编辑:陶家龙来源:https://urlify.cn/J3mAna