ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

京东APP后台多端融合架构代码重构实战

京东APP后台多端融合架构代码重构实战 简介重构是一个非常常见且古老的课题涉及重构的文章、书更是不可胜数。但其实做程序做久了就会知道想把一个复杂的系统做好尤其是参与人数较多的中大型项目靠看几本设计模式的书去试图寻找设计模式的奥秘其实是不够的。很多时候看书时觉得很有道理例子也能理解但到实际开发时却无从下手不知道怎么灵活套用。很多项目在持续的版本迭代中还伴随着人员的更替过程往往为了解决眼前的需求最常见的就是直接复制类似的逻辑或者就是在末尾追加逻辑。同时受限于对老版本的需求理解很容易出现新需求覆盖老需求的参数值无意识的更改老版本结果等。那么倘若系统的隔离性做的不好则极易产生A功能的改动影响BCD等一大堆功能的正确性。以京东App的后台为例就是一个典型的复杂系统涉及开发人员众多模块巨多迭代时间很长已近10年很多业务逻辑已无从考证开发人员也已经换了好几轮那么对于这样的系统如何让开发人员做的需求、功能隔离开互不影响各小模块又能各自健壮、系统又具备相当的扩展性、配置化率又高仅通过配置即可完成功能的变更就成了一个不得不深入考虑的问题。本篇主要是我在实际工作中对一个复杂系统做了一些改造并从中总结的一些经验做的分享。案例近期京东App后台核心模块发生了较大的逻辑改动主要原因是新增了一些接入来源从之前的独苗京东App到后来的京东PC站、京东极速版、老年版、小程序等等都接入了原App后台。通过完成统一接入避免了多个后台共存重复开发的问题。而这些不同的来源逻辑就有较多不同。有的来源需要执行逻辑ABC有的来源需要执行BCD有的只需要执行BC并且不同的来源返回值也有所不同这就对之前的单一来源的系统架构产生了较大的冲击如果处理不当则不可避免地出现大量的if-else逻辑以及扩展上的混乱。那么针对这种情况以及对提高系统整体配置化率的诉求我们对后台架构做了一次重构。本文就是对重构内容做的一个浓缩后的抽象讲解线上实战性质非单纯设计模式类的demo。如下图我尽可能简化了细节和小模块内的逻辑仅保留了最外层的大模块。我们来看背景之前只有主App客户端来源的请求譬如refer1该请求到达后需要触发运费、优惠券等数十个上游rpc调用之后聚合各上游系统结果返回给客户端对应结果。现在新增的接入方譬如老年版refer5就删减了组件层一些复杂的促销逻辑、凑单、白条之类结果层也有相应删改。我们该如何支撑这种可能随时增删改模块和来源的业务架构呢原始问题点——复杂的排列组合当多个层级均出现了多个变量时这个系统的逻辑就变成了一个复杂的排列组合问题。我们假如用户的入参是User对象里面有一个字段refer表明了来源。在重构前代码进入主流程后如下图就是简单地根据refer来决定是否走哪些模块、返回哪些参数。图中仅作为示例实际情况每个 fetch模块都有数千数万行代码逻辑之复杂各种if、else运用之嵌套各种与或非使用之犀利实属鲁班再世也要夸赞几句鬼斧神工的。大家都能看出来的问题就是业务模块与入参变量的强耦合如果入参refer1则执行业务模块A、B、C不执行E、F、G且返回值包含X、Y、Z不包含U、V、W。这样的设计不可避免带来了极大的维护的麻烦与混乱到处都在判断是否是它是他还是她那么该如何隔离层级解耦模块与来源、来源与返回值之间的关联呢。简化判断让职责单一上面提到了最大的问题我们用通俗的话来讲就是如果是A我就做A1、A2、A3如果是B我就做A2A3A4。那么问题就是这个主逻辑器做了太多的事情日后被修改的概率极大每个逻辑变动都会导致主逻辑器的改动。我们主要优化的点就是将这个逻辑给去掉让主逻辑器职责单一每个业务单元也职责单一。将上面的逻辑变成如果是A1则来源是A时我工作是B时我不工作如果是A2则来源A、B我都工作如果是A4则来源A我不工作来源B我工作。可以看到做的事情就是当有一堆条件判断要决定执行N个逻辑中的M个时调用者不应该关心调用逻辑而应由这N个逻辑自行判断自己要不要执行。从代码实现来看就是调用者不关心有多少个逻辑块也不必关心日后的增减从而实现调用的解耦。那么代码该如何写呢原来的是在一个方法里fetchStockfetchDiscount等等首先我们要把这些实现全部去除并统一为对接口的遍历。改造后的代码是这样代码很简单注入一个接口的集合并遍历这个集合根据实现类返回的true、false决定是否要执行这个实现类的业务逻辑。接口定义如下单个逻辑单元代码如下以上主流程的逻辑很清晰后续随着各模块的扩展或缩减都不需要动主逻辑而只需要各个子模块根据自己的情况返回是否要执行自己即可。动态配置避免硬编码通过以上的改造我们已经完成了模块间的隔离当有新增、删减模块时可以做到不影响主流程且将代码修改、影响范围控制在一个类里。但是需求的变化总是很频繁仅仅做到互不影响还不满足需求我们还需要做到能够动态的控制各个模块的启用和关闭。譬如『如果是A我就做A1、A2、A3如果是B我就做A2A3A4』。希望能做到随时仅通过修改配置不改代码不重新发布而做到『如果是A我就做A1、A2如果是B我就做A3A4』完成对模块的启停。动态配置该如何实现呢其实很简单我们只需要修改execute方法将refer1这种规则存放于配置中心将execute方法里的硬编码判断变成根据配置中心的配置进行判断即可。如下图那么ConfigCenter就是配置中心工具类里面提供了根据key获取value的方法。配置中心大抵如注释所描述的在应用启动时全量从zk、etcd等拉取配置并保存在本地内存并开启监听当配置中心内容有变化时更新到本地内存里。通过观察各个模块的execute类可以进一步发现倘若配置中心里我们将类名作为key模块所支持的refer集合为value时各模块的execute方法就是完全一样的代码。那么整个方法又可以进一步抽成一个抽象类由该抽象类来完成这个判断逻辑如图当有了统一完成开关的父类后则各个模块的逻辑单元就更加简单了只需要关注自己的业务逻辑即可。至此最小业务单元职责则回归到纯粹的业务逻辑不再参与流程控制的逻辑判断。同理主流程也不再参与对各个子业务单元的判断和控制只关注于对接口的遍历各模块也不再产生相互影响。结果层字段隔离控制以上我们完成了业务逻辑单元的隔离那么对于结果层该如果控制呢原始代码是这样的在主流程中对各个变量进行判断然后设置结果的值。从原始代码可以看到即便只有一个变量refer就已经让代码可维护性变的很差更别提真实场景下变量可能有多个时要维护不同变量场景下返回不同的参数该多少困难。通过对上面业务层控制的实现我们同样可以采用类似的方式来处理结果层。定义一个接口如下定义boolean型方法让各字段决定自己要不要返回。定义key、value用来存放字段名和value。修改主流程如下当需要返回时才能待返回字段的key、value存起来。实现类如下以上方式展示了对结果层进行精细化控制的简单方案实际场景中可能涉及结果层数据结构并不是单层、对key、value的判断需要额外的属性等其实思路都是一样的。如果要增加入参的判断在接口的needOut里追加要参加逻辑判断的入参即可。如果返回的结构不是单层的key-value则在复合结构的实现类里再嵌套一层新定义的接口的遍历也可。长逻辑相关处理长逻辑这种最常见也是最好处理的。我们经常在写一处逻辑时刚开始很简单几行就解决了后来随着业务越来越膨胀这个方法也是越来越长。终于有一天代码长到显示器装不下它了后面的逻辑开始出现对前面的逻辑产生影响了这个方法就开始变的有坏味道。这种相信大家都不少见尤其在老系统中从1千到8千行的我都见过编辑器右边的滚动条都要看不到了。当然仅仅是长倒还好说主要的问题是相互影响前面赋的值后面就被覆盖了这种问题往往还比较隐蔽极其影响系统的健康。解决这种长逻辑其实很简单做好两件事即可1-将方法的顺序执行变成接口遍历2-封装。1 如何将方法的顺序执行变成遍历呢这个其实在上面已经讲了是类似的做法。将一大堆在同一个大方法里的小方法全部变成某个接口的实现类从而将方法的顺序执行变成对接口集合的遍历执行。后续增加或删减方法时只操作对应的类即可而不需要对这个大流程做修改。一个单独的实现类就是这样的可能有人会问我的各个方法是有先后顺序的你用了接口集合该怎么控制顺序呢从上图的Order注解可以看到这个就是控制在接口实现类的顺序的值越小在List里越靠前。2 如何理解封装呢这个更简单之前不是说代码长了易出现值被后面的逻辑覆盖那么就以某个最小参数为一个类所有对他的增删改都控制在一个类完成对某参数、对象的封装控制。而不要散落各地去修改一个参数的值。带中断的长逻辑处理最后一个问题如果带有流程中断的情况。如图一个长逻辑在某些条件被中断了中断后后面的逻辑自然是走不到了。那么之前的对接口集合遍历方式还能用吗自然是可以用的不过就要稍加改造让实现类的方法返回一个boolean值当false时中断这个循环流程即可这样后面的逻辑就走不到了。如果不是要中断而是某条件下执行自己某条件下跳过自己这个就往上看看文章的第4段。最后本文通过一些例子描述了一些场景下对系统的改造方式由于京东APP后台逻辑复杂以上场景仅覆盖了部分典型场景未全部写出改造点当然还有一部分是特有的非典型问题可能大部分用户碰不到的场景也未写出。如果有遇到类似场景可参考文中的一些方式进行处理。作者平台研发武伟峰
返回列表