网站设计说明书800字,广告喷绘制作公司介绍,俱乐部logo免费设计在线生成,万维网的代表网站人人都讨厌代码腐化#xff0c;人人都在腐化代码#xff01;本文介绍app消息推送开权提醒能力的服务端实现#xff0c;并说明如何通过手搓一个简易的流程引擎来实现横向的业务场景隔离#xff0c;纵向的业务流程编排#xff0c;从而灵活支持业务需求#xff0c;抑制代码腐…人人都讨厌代码腐化人人都在腐化代码本文介绍app消息推送开权提醒能力的服务端实现并说明如何通过手搓一个简易的流程引擎来实现横向的业务场景隔离纵向的业务流程编排从而灵活支持业务需求抑制代码腐化。
背景
消息推送是电商APP引流促活的重要手段如何引导用户打开消息推送权限接收APP的推送消息是各大电商APP都需要思考的问题。消息平台在过去的两个季度通过技术手段引导了近百万的用户开启推送权限在用户关闭app消息推送权限情况下在某些特定页面提示用户去打开权限。开权提示又可以分为弱提示强提示。
弱提示如下只展示开权提醒。 强提示如下展示开权弹框。 产品需求
从产品侧来分析需求我们希望能够有效引导用户开权同时又不能过于频繁的打扰用户引起用户反感。因此我们的提示要有针对性且要能够控制提示频率具体需求点如下
不同页面提示文案不同第一期迭代支持【消息】【商品】【订单】三个页面的开权提醒。每个页面的提示文案不同。如消息页面提示“开启消息通知互动消息、优惠活动不错过~”订单页面提示“开启消息通知及时了解订单物流状态”等等。提示文案支持动态配置业务方需要修改提示文案时可以通过修改配置快速生效。提示次数要可控不能频繁提示打扰用户。 一天最多一次一周内最多n次n可配置个别页面可以永久提示不同页面的频控次数可配置用户主动关闭提示后一段时间内不能再提示
请求流程
用户进入特定页面后客户端判断若用户未打开推送权限并且满足某些特定业务需求比如在商品页面若用户主动收藏过该商品则认为需要给用户开权提示。此时客户端调用服务端接口来获取开权提示的文案。服务端接收到请求会先进行防疲劳判断比如本周该用户看到过开权提示的次数超过7次了那就不再提示。当然不同页面防疲劳规则也不同对于消息页面业务就需要开权提示一直展示。
获取提示文案流程 客户端获取开权提示后会在业务页面展示出来并且回调服务端告诉服务端该用户在某个页面成功展示过一次提示。用户看到提示后可以选择去打开推送权限也可以忽略甚至关闭掉开权提示。这时候客户端需要回调给服务端记录用户主动关闭一次提示。用户关闭超过3次则半年内不再提示。当然不同的业务具体规则会有变化且要支持可配置。
提示回调流程 服务端设计
只考虑单一页面的提示那么方案很简单流程如下 伪代码如下
public String getTip(String page){获取配置();if(单页面防疲劳()){return ;}获取提示文案();修改单页面防疲劳数据();修改全局防疲劳数据();}
如果需求变化不大只有一个页面需要开权提示那么以上方案完全可以满足。但是对于app开权提醒绝不可能只在一个页面提示且不同页面的提示文案频控方案都不相同在这种需求背景下以上的方案有哪些弊端呢
代码耦合度高易腐化
比如新增一个页面订单页面他不需要全局防疲劳怎么改呢需要在主流程中增加订单页面相关的判断条件伪代码如下。新增页面越来越多的情况下代码会在一两个迭代周期内迅速腐化连开发者自己都看不明白逻辑。 代码腐化的主要原因 缺乏设计。缺少必要的封装和抽象代码逻辑完全是从业务逻辑直接翻译过来的也就是直译型代码。这类代码直观直接但是难以应对业务变化一写出来就已经腐化了。 public String getTip(String page){获取配置();if(单页面防疲劳()){return ;}if(!订单页面()){if(全局防疲劳()){return ;}}获取提示文案();修改单页面防疲劳数据();if(!订单页面()){修改全局防疲劳数据();}}
测试成本高
还是以新增页面来看对于测试来说他不仅需要测试新页面的功能而且还必须要回归老的消息页面的功能因为新页面的改动影响了原有的业务流程。 我们都知道面向对象的开闭原则对扩展开放对修改关闭。对扩展开放好理解因为需求一直在变我们的代码必须能够灵活扩展以适应变化。对修改封闭我们的扩展尽量不对现有的代码改动太大。为什么因为修改意味着成本上升。成本不仅仅是代码实现维护的成本还包括测试的成本。 不易扩展难以应对变化
考虑以下几个变化a. 业务需要临时关闭某个页面的提示而客户端又来不及发版 b. 调整某个页面的防疲劳次数且不影响其他页面现有的防疲劳c. 某个页面需要对防疲劳规则做ab实验如何修改代码影响效率最高影响最小。对于目前的方案来说这类需求都需要对主流程进行改动必然可能会影响其他页面的功能从而导致线上功能不稳定。
解决方案
上述几个问题相信大家都耳熟能详根本原因还是代码设计过程中未充分解耦随着迭代推进最终导致代码腐化难以维护。下面分享一个比较有效的解耦设计横向业务隔离纵向流程编排。
场景隔离
我们用一个抽象的模型来描述这类需求。如下图有A,B,C...等多个场景。每个场景都有step1, step2, step3,step4...等等多个步骤不同场景可能有不同的步骤同一个步骤在不同场景的实现可能也有细微差别。在未解耦前我们的代码结构如下图所示各个场景的代码通过判断语句耦合在一起。 伪代码如下
public void execute(String scene){if(!scene.equals(C)){step1();}if(scene.equals(A)){step2();}if(scene.equals(A)){//判断语句耦合不同场景的逻辑step3();}else if(scene.equals(B)){step3_EXT_B();}else if(scene.equals(C)){step3_EXT_C();}}
解耦后的效果如下各场景在逻辑上是隔离的每个场景有自己的业务流程。 这样解耦的好处如下
各个场景的业务流程在逻辑上是相互隔离的不会因为修改某个场景逻辑导致所有其他场景的代码都受影响。各场景之间的步骤可以抽象为action实例相同action可以实现快速复用。快速支持新场景新场景只要提供新的场景流程实现即可对已有场景无影响且测试时无需回归全部场景极大降低测试成本。
想法很好那如何落地呢答案是手搓流程引擎
流程引擎
这里的流程引擎不是Activiti, JBPM这类重量级流程编排工具相对我们的需求来说使用这类工具有点大材小用而且有过度设计的嫌疑反而会大大增加开发成本。 代码腐化的另外两个原因 过度设计。多余的设计不仅不产生业务价值而且无端提升理解维护成本尤其需求之外的功能代码本身就是已腐化的死代码针对这类代码要尽早做减法应删尽删。设计弃用。软件开发经常会碰到有些同学拿着电锯当菜刀使。比如已经引入ORM框架他还是要手写SQL比如有了AOP他还是要到处嵌入重复代码。这类做法也会加剧代码腐化。 根据需求我们只需要实现一个轻量级的流程编排工具帮我们实现如下两点能力
在横向上通过不同的流程上下文装配各自的步骤节点action把不同场景的逻辑隔离开来。在纵向上通过在流程上下文中的节点处理器handler执行各流程的业务步骤。提供上下文工厂类根据场景code来提供不同场景的上下文实例。
整体结构如下 流程装配
新增场景时提供场景上下文生成器实现类在generate方法中装配流程步骤Action类实例。
Servicepublic class AGeneratorP, R implements IContextGeneratorP, R {private final ProcessAwareContext processAwareContext;public AGenerator(ProcessAwareContext processAwareContext) {this.processAwareContext processAwareContext;}Overridepublic boolean check(P paramDTO) {return false;}Overridepublic ProcessContextP, R generate(P para) throws Exception {ProcessContextP, R context new ProcessContext(para);context.addAction(processAwareContext.getBean(Step1Action.class));context.addAction(processAwareContext.getBean(Step2Action.class));context.addAction(processAwareContext.getBean(Step3Action.class));return context;}}
场景流程的各个步骤用Action类来封装。通过步骤的封装实现业务步骤在不同场景流程中的复用。Action类型又可以分为以下几类
网关节点控制流程是否继续执行的节点比如判断用户开权提示已经被频控拦截了那么可以快速结束流程值节点修改流程返回值的节点即对流程返回值有影响的节点。空节点对流程走向和返回值都无影响的节点比如缓存数据写数据库的节点。
上下文装配流程步骤时根据步骤实例Action的类型提供相应的执行器在引擎执行时调用处理器来处理Action对应的业务操作。
每个节点类型都有对应的处理器。
网关处理器执行网关节点。网关节点的执行结果是一个布尔值用于判断该流程是否结束如果结束则停止后续节点执行。目前的网关处理器设计比较轻量后续业务有需要可以支持复杂的网关设计比如根据网关节点的返回值控制流程的执行路径等能力。值节点处理器执行值节点。值节点的执行结果会被设置到上下文对象的result字段从而设置整个流程的返回值。空节点处理器执行空节点。这些节点不影响节点执行也不影响流程结果主要用于修改缓存数据库等数据操作。 流程执行
执行场景流程时从流程上下文工厂类获取流程上下文实例提交给流程引擎执行。
Overridepublic R execute(P paramDTO) {if (!validParam(paramDTO)) {return null;}try {ProcessContextP, R context processFactory.get(paramDTO);ProcessEngine.execute(context);return context.getResult();} catch (Exception ex) {log.error(processExecuteFailed, ex);}return null;}
工厂类只有一个get方法根据场景参数获取场景对应的上下文生成器由生成器动态装配出该场景对应的上下文对象。 Service
public class ProcessFactoryP, R {
ListIContextGenerator generators;
public ProcessContextP, R get(P p){
try {
IContextGenerator generator generators.stream().filter(ig - ig.check(p)).findFirst().get();
if (generator ! null) {
return generator.generate(p);
}
}catch (Exception ex){
}
return null;
}
}
引擎执行的逻辑很简单从上下文对象中获取流程中的各个Action对象的处理器依次执行处理器的handle方法从而完成流程步骤的执行。 public static void execute(ProcessContext context) {
if (Collections.isEmpty(context.getHandlers())) {
return;
}
context.getHandlers().forEach(handler - {
if (!context.isDone()) {//快速结束
((BaseHandler) handler).handle(context);
}
});
}
收益与展望
推送开权引导能力一期支持3个场景对开权率有明显的提升效果因此迅速吸引数十个不同场景接入。大多数场景的业务流程都是一样的可以通过复用既有的流程上下文生成器生成各自的流程实例来处理。对于有特殊防疲劳逻辑的场景可以通过拼装各自的流程上下文来实现。比如
【消息中心】场景不需要全局防疲劳亦即【消息中心】的展示次数不受全局其他场景展示次数影响
【我的购买页】需要针对不同的ab实验支持不同的防疲劳逻辑
这些场景可以通过提供各自的上下文生成器的实现类来实现不同业务流程的编排且新的流程不影响既有流程的执行逻辑。如此我们便实现了面向对象编程的开闭原则既对新增的流程扩展开放对已有的流程改变关闭充分满足了目前多个业务场景的开权提示需求。
当然这个方案远非完美比如新增业务流程时我们依然需要修改代码通过代码去编排业务流程是否可以通过修改配置流实现流程编排甚至通过在画布上拖拽节点来实现流程编排等等。这些就涉及到通用流程编排引擎的范畴了有兴趣的同学可以参考activiti, JBPM这些通用流程引擎的实现此处不做赘述。
总结
以上介绍了APP消息推送开权提示的背景和实现逻辑并说明了服务端如何通过流程引擎实现对业务场景的隔离从而达到降低维护、测试成本抑制代码腐化的目的。事实上业务场景隔离本身并不复杂方法也很多除了本文介绍的流程隔离外还可以借助接口隔离依赖包隔离甚至微服务隔离等多种形式。核心问题在于如何预知业务的潜在变化提前合理设计而不要等变化发生后才去重构事后重构往往意味着不重构。
限于水平文尽于此欢迎大家批评指正。