1 简介
UI 的全称是 User Interface ,但公司里的通用定义是:用于 web 类接入服务的后端接入模块,常用于用户身份校验、页面渲染和协议转换等功能,位置介于 web 服务器和逻辑服务器之间。在 Webim 系统结构中,由于前后端基于数据分离,即后端只是提供数据,由前端 js 完成页面渲染,因此 Webim 系统内的 UI 更多地承担了身份校验和协议转换的功能。
2 重构
2.1 重构之前
重构前 WEBIM-UI 主要存在的问题如下:
1. 代码庞大,组织较混乱,最大的一个源文件代码超过 1w 行。
2. 流程不够清晰,指令流程与具体实现耦合较紧,导致修改指令处理流程和新增指令较为困
难,特别是后期同时支持 bridge 和普通用户时这一点尤为突出。
3. 公共逻辑代码抽象的不够,很多相同的逻辑重复实现,冗余代码较多,增加了维护的难度。
4. 随着业务需求的不断变化,已有的接口设计不能满足需要,需要进行接口层面的扩展。
如之前单一的根据后端回包中的 code 决定给前端回包的 result ,随着需求实现协议的增多,会出现不同协议返回相同 code 代表不同语义,需要输出不同的 result 。
5. 为了解决前端指令与后端接口存在先天的矛盾,需要 UI 从结构上灵活的支持组合指令。
产品为了提高用户体验,前端设计的趋势是要求大而全的接口,尽量减少与后端通信交互的次数。
而 UI 与后端服务的接口更注重的是接口语义的原子性,因此对 UI 的结构提出了更高的要求,需要能轻易地支持前端指令与多条后端接口的映射。
2.2 如何重构
在重构前花了一段时间整理所有指令的处理流程,事实证明这一步是非常有必要的。整理后把
系统的指令分为几类: welcome( 登陆 ) 、 transfer( 通用上行指令 ) 、 logout( 登出 ) 、 pick( 异步获取消息 ) 、 read( 其它 ) ,其中划为 transfer 的指令占据了绝大部分。除去相对特殊的 welcome 指令, transfer/logout/read 的指令处理流程相对类似,大致可以分为两步:解析上行请求组装发送后端请求包 (pack) 和 解析后端应答包格式化输出 json(format) 。区别在于组装的后端请求包格式不同,并且 transfer 的解析 / 格式化过程相对复杂,可能涉及到 username <–> uid 的转换。对于 pick ,可以把该指令进行划分成上下行:上行 pick 用于注册通知通道及保持用户在线,下行 notify 用于处理通知消息。经过这样划分, pick 的处理流程与 transfer 等指令几乎一致。
对于 transfer 类指令,单独整理了其实现流程,流程图如下:
从流程图可以看出, transfer 类型各条指令的区别在于:解析请求上行组装协议 xml(pack) 和 把后端响应包转化为 json(format) 。通过提取出通用的 pack 和 format 接口,分别建立 pack 和 format 的映射表,建立指令名称 (command) -> 指令对应接口实现 (function) 的映射关系,即可屏蔽掉这层差异,保证处理流程的统一。在 format 过程中,发现 json 回包中 result 和 errorMessage 字段由后端响应包中的 code 和指令名称 (command) 决定。对这部分实现配置化,避免了代码中出现过多的 if/else 条件分支、新增指令时对程序的修改,便于模块的后续维护和升级。
在 transfer 指令处理流程统一的前提下,组合指令的实现就变得水到渠成了。让每条组合指令从配置中读取需要执行的单条指令序列列表,遍历列表顺序执行每条指令,汇总执行结果输出即可。
组合指令的处理流程如下:
目前实现的组合指令相对比较简单,只是把多条指令的执行流程串行化,参数都在初始时全部传入,各条指令分别从初始的请求中解析参数,把各条指令的 json 包追加到结果中。即组合指令流程序列中各条指令没有明显的逻辑依赖关系,可以任意排列先后顺序。对于部分关键字段如 uid 做了特殊处理,会自动解析前一条指令应答包中的 uid ,作为下一条指令的请求参数。由于所有指令从公共数据存储 (mc_pack_t) 解析请求,并没有按请求区分参数,如果不同的请求参数重名时,会出现该参数无法区分的情形。
3 发散
考虑到当前的业务需求,组合指令目前的实现方式比较简单,还有较大的改进空间。初步整理了下,大致的改进点如下: 1 、为每条指令建立单独的请求参数存储; 2 、支持变化的指令序列,可以根据初始的请求参数决定指令序列; 3 、支持组合分页类的指令,即允许单条指令在组合列表中执行多次。
仔细深究组合指令的执行流程,发现与设计模式中的管道 - 过滤器模式比较类似。基于管道 - 过滤器模式的系统由管道和过滤器组成,每个处理步骤被封装成一个过滤器组件,数据在相邻过滤器之间通过管道传输。每个过滤器可以单独修改,功能相对单一,顺序可配置。
针对管道和过滤器实现时一些关键的点,结合组合指令的实现方式进行分析如下:
l 分解系统任务成一系列的处理阶段
管道和过滤器实现的前提,在于把系统任务分割为相对独立的任务,任务之间没有明显的逻辑
依赖关系,仅关注前一个任务的输出数据流。这样通过数据流把各个任务串联起来,可以灵活替换某个步骤,实现不同的结果输出。 WEBIM-UI 中对组合指令按照后端独立接口分解成不同的指令,每条指令的逻辑处理可以看作一个过滤器,过滤器之间管道传输的数据流由三部分组成:去除前一指令解析参数后的请求、前一指令输出的部分参数 ( 可选 ) 和前一指令输出的中间结果。
l 定义沿管道传输的数据格式
对于每个过滤器,考虑到后续的扩展和灵活性,倾向于制定一个统一的数据格式。 WEBIM-UI
中指令之间传输的请求数据通过 mc_pack_t 表示,中间结果采用 json 表述。
l 如何实现过滤器链
所有的过滤器都服从统一的过滤器调用接口,则形成了过滤器链 (Filter Chain) ,常见过滤器链
连接是采用推的方式实现的,即在前一个过滤器流程运行结束时主动调用 FilterChain.next() 转到下一个过滤器。 WEBIM-UI 里采用维护指令列表下标的方式来标记当前运行的指令,在指令处理结束时对下标操作,指向下一步需要执行的指令。
l 设计和实现过滤器
对过滤器抽象出公共的 interface ,仔细划分每个过滤器的功能,实现对应接口即可。 WEBIM-UI
中抽象出 pack 和 format 接口,建立每条指令到这两个接口实现的映射。
l 建立流水处理线
过滤器的部署可以通过配置文件指定其调用顺序, WEBIM-UI 中在配置文件中指定组合指令对
应的指令列表和各条指令的顺序。
除了这些共同点, WEBIM-UI 组合指令的实现与管道 - 过滤器的原则有一些区别,主要的区别在于目前组合指令的实现会共享状态信息,各条指令运行过程中会共同使用同一个上下文。
4 总结
有了清晰的重构目标,结合当前的业务需求,在深入了解业务流程的基础上抽象出公共的流程,
模块的重构之路变得简单很多。