客户端模块化解耦实践 - Router

1,241 阅读8分钟

客户端模块化解耦实践 - Router


背景

随着业务进入快速发展期,业务线拓展迅速,项目结构变得庞大复杂,导致迭代的成本越来越高。项目的越发庞大也使得整个工程编译时间越来越长。进行项目拆分后模块化运行(项目自运行、自管理)是一个较好的出路,但项目间的复杂相互引用导致我们无从下手,而如何去除这些项目间的引用则是今天的主题

产生

首先我们可以整理下现状,我们的碰到的最大问题其实就是项目间杂乱无章的耦合、引用


actu-structor

针对这些耦合、依赖,我们可以简单的分析:从理论上来说,每个项目负责单独的业务,应该本身就是相互独立的存在。但是实际上,处于同一个公司业务体系中,项目与项目之间不可能真正做到完全的切割。(公司与公司间都可能存在合作,项目与项目间当然就更可能了)

看来想从业务源头想去除业务依赖是不太可能了,我们只能从我们技术角度去进行项目间耦合、引用的去除了。而其中最关键的一点就是:耦合、相互依赖 , 而Router的概念正是为了解决这些问题而提出的。

那Router到底是什么呢?

Router是一个具有收口分发思想的、用于处理模块(项目)间直接依赖、调用的模型。最直接的体现其实就是一个约定、一个规则,按照约定规则进行解析,而后分发。


hope-router

发展

针对以上的一些问题,其实我们能get到一些关键特性,收口分发

OK,既然关键信息已经得到,伪代码很快就能敲出:

    public static void jump(String rule){
        if(rule){
            gotoBusinessActivity()
            return;
        }

        if(rule){
            invokeBusinessFunction();
            return;
        }
    }

Perfect!!! 调用方便,确实做到了 收口分发

但是随着业务发展,弊端显示的也越发明显。仔细分析过后,可以总结出一些问题

膨胀

  • 随着业务的增长,膨胀的太厉害,成千上万行代码坐落在这个类中,名副其实的大杂烩

维护成本

  • 所有项目均同时维护,一个项目改错可能导致其他项目也受到影响
  • if-else的逻辑维护太复杂
  • 分发逻辑是 hardcode ,一旦错误将无法进行修改
  • 各项目无法真正分离

扩展

  • 从代码结构不难看出,这里的分发实质上是'亲力亲为'并非真正的分发,而‘亲力亲为’是做不到真正的拆分的

从上面几个问题来看,其实收口并不难,如何做好分发这才是一件难事。

回顾需求与现有的问题,觉得自己的if-else模型其实丢了一件事:没有抽离出一套统一的流程

那流程怎么定义?

我在设计的时候喜欢类比现实生活,而Router也不外乎。而这个模型就很有趣了

我想要寄快递

这个是我需求,那么该怎么办呢?最重要的两点

  1. 地址
  2. 包裹

我只需要给到快递员地址以及包裹,那么就会给我送到(除非地址错了),但是这里要强调一点,我寄快递,并不是快递员来接受这个包裹。快递员只是,而真正接收的是所在地址的人。

想到这里其实就可以梳理出Router的核心生命周期与非核心生命周期了。当Router把调用者的意图转达给接受者时就已经完成了它的周期. 接下来的事就是接受者干的了,跟Router无关


invoke

到这里其实Router的基本框架已经确定(因为流程已经抽离), 那么剩下的就是具体模块的事了。不难分析出Router的两大核心模块:规则处理执行器

规则

通过模型,我们知道了两个必须的东西:地址包裹 ,那对应到程序中无非就是 目标指向数据 ,再契合到我们当前的业务场景,规则不难提炼出来:

protocol://path?data

是不是很眼熟?其实就是我们通用url的简化版本。其实重点就三个信息:

  1. 协议,用于版本区分、功能区分等
  2. 目标指向,路径,意图的接受者
  3. 数据,携带的数据

同样我们的规则定义为

tctclient://project/module?key=value

协议定义完毕了,但是难点来了: Path的映射如何处理

举个例子,Pathhotel/list ,但是这个只是一个字符串,我真正想要调用的是 酒店列表 HotelListAction ,如何将 Path目标指向 进行关联,这是一个问题。在讨论的过程中,其实有三种方案:

  1. if-else 直接逻辑关联
  2. 使用 Map 进行关联
  3. 使用 xml 进行关联

其实从本质上来说,都一样,都是为了数据关联。但是在维护、扩展上是有区别的。

if-else 这个不用说,直接的引用会产生的非常复杂的依赖网,而且无法抽离出分发这块,所有的分发都需要在当前环境下进行关联。

Map & XML的逻辑很像,都是 path处理类 的对应。但是在维护和扩展性上我们最终选择了xml, 优点很明显:无代码耦合可动态更新 。而MAP的优点在于,不需要进行数据加载,节省了这一部分的性能。

执行器

执行器的概念就好理解多了,当一段规则被解析成可读的意图后,我需要将意图、数据传递给接受者,而这个接受者可能是各个业务所处理的。因此我需要设计一个接口(不然没指向性了)来进行接收,而业务部门可通过实现这个接口来进行处理相关逻辑。


flow1

虽说到这里流程基本已经结束,但是在实际的业务逻辑中会衍生出一些特殊的逻辑,比如:

我要打开酒店列表,但是有个要求,必须是登陆状态才可直接进入,否则需要先去登陆。

可能仅仅从这一个需求上来看,用现在的流程去完成也不难,可以使用两个接收者去完成。但是当中间衍生的特殊逻辑很多而且可能多个业务都会需要,那用两个(多个)接收者可能就有会膨胀的很厉害了。那这个时候其实另外一个概念就产生了 -- 拦截器

拦截器

拦截器是当一个规则产生最终作用前进行的一些特殊而通用的逻辑处理。

处理逻辑其实很像View Touch事件,当一个Touch事件产生后,首先是分配(分发),然后是拦截,最后才是消费

而我们的拦截器稍有不同的地方是

  1. 拦截器可以多个
  2. 只有当所有拦截器全部满足(pass)的情况下才会走到执行器(消费)
  3. 这种拦截器模型可以通过递归来进行实现

flow-interceptor

拓展

对于Router框架设计到使用到现在已经2年多了,业务需求等也都基本可以完成、满足。也确实做到了Router框架抽离、流程确定且业务逻辑(包括映射关系)各自维护。当一个业务需求产生时,只需要产生一个新的规则来维护即可。

但是重新审视下目前的框架其实仍然有很多需要完善的:

  1. 数据映射是通过xml来映射,那么必然会带来I/O加载的性能开销
  2. 是否项目调用需要写一段很冗长的规则,比如 jump("tctclient://hotel/list"),而这种字符串参数通常是无法通过编译器去检查正确与否的

针对这两个问题,其实我们采用的是(插件)工具去完成。

Gradle Plugin + Freemarker

在编译时进行辅助类的建立

  • xml的加载可以在编译时直接生成Map关系维护。
  • 冗长规则则可以通过枚举来进行维护(枚举通过xml生成), 调用方式则改为jump(Bridge.HOTEL_LIST)

总结

该篇文章更多的写的是我在接受到这个需求时的心路历程,每个人在写代码的时候都有自己的感受。而从设计Router中其实能发现很多道理

没有一个模型放之四海而皆准的

比如一开始的 if-else ,当业务量很小的时候 , 这可能是一个非常好的处理方式。而当业务膨胀,当前结构不适用时需及时重构。

框架脱离了业务那就什么都不是,只有契合业务的框架,才是一个好框架。

思考比编码更重要

当接受到一个需求后,更多的是需要去思考,去设计。提炼出核心关键点、流程,针对流程梳理出核心生命周期非核心生命周期很重要