客户端模块化的精益求精

猪小花1号2018-09-13 11:34

作者:韩坤芳


一、序

客户端的模块化、组件化的概念,一点都不陌生。每个公司、每个产品,无论大小或多或少都在做模块化相关的工作,甚至可以说,不做模块化,都不好意思说参加过企业级的项目。 

到底是模块化,还是组件化?我个人觉得一种解释还是比较合理的:模块更偏于业务模块(比如订单模块、商品模块),组件更偏于技术类的模块(比如网络库组件、图片库组件),模块的颗粒度可能会更大一些,组件的颗粒度相对小一些。Android 中对于多模块的开发,英文就叫 Module。咱们这篇文章里,姑且都叫模块吧。 

模块化的路是长久的、持续的,不同的业务产品,实际需求可能会不太一样,咱们撇开特性,来说说模块化共性方面的一些探索与精益求精。


二、大纲

  • 模块化重要性
    • 思想同步,一致下我们探讨话题的意义
  • 模块化架构支撑
    • 模块化架构理念,设计思想
  • 模块的设计
    • 本文的重点,结合我们目前的痛点,探寻如何设计一个“拿得出手”的模块
  • 小结:健康模块属性


三、模块化的重要性

引用 Tim Berners-Lee 的一句话:

简单性和模块化是软件工程的基石;分布式和容错性是互联网的生命。

  • 降低大型软件复杂性和耦合度
    一个很庞大的系统,信息量之多会超过人脑的处理能力,进而失去分析能力。将庞大的系统通过业务、功能的边界划分,确定一个个子系统和组件,整个系统的架构一目了然。

  • 模块重用、模块重组
    避免重复造轮子,达到技术结果价值最大化

  • 微服务

    • 多团队并行开发测试
    • 可单独编译打包某一模块,提升开发效率


四、模块化架构支撑

  • 成熟的软件架构思想
    以下几个概念:OSGI、SPI、DI、微服务、CS 模式、事件机制等想必已经耳熟能详,不清楚的可自行 google。
    客户端的模块化整体思路无非就是这些思想的汲取。

  • 模块的梳理,依赖关系
    技术模块的确定,首要的源头便是业务模块的划分和抽象。所以在做任何业务模块的之前,首先从需求的源头来圈住业务边界。模块颗粒度的控制,边界的确定。
    模块需要分层,同一层级的模块应避免依赖,严禁反向依赖

  • 模块之间的通讯
    一般 module 之间的通讯有以下几种:

    • 模块之间的页面互调
      路由,委托代理机制
    • 某种事情的通知
      Eventbus,事件监听机制
    • 直接调用业务模块的业务能力
      委托代理机制,依赖注入


  • 模块注册,初始化                       

  • 模块内部设计思想

    • MVP,MVVM


五、模块的设计

本小节的大致思路是这样的:简述当前模块遇到的一些痛点,针对这些痛点,给出我们通常的解决方案,最后以一个功能模块作为实战案例来收尾。 

以网易卡搭编程APP 为例,iOS 有 20+ 模块,Android 有 30+ 模块,模块的数量不算太多,但也不少。这些模块中,有些是教育产品部门几个产品(云课堂,中M,企业云等)共用的,有些是单独卡搭编程APP自己的。以这些模块作为样本,整理了下模块通常会遇到的一些痛点以及策略: 

  • 同一层级的模块通讯

    同一层级的模块A,模块B,谁也不依赖谁,如何互相通讯。

    如何解决?一般有以下几种方式:

    • 代理模式(主工程代理模式)
    • 路由模式

  • 模块全家桶模式

    如果想要依赖一个模块A,然后就间接引入了模块A 依赖的模块 B,模块C,1 拖 N。
    这边 1 拖 N 带入的模块,主要有两大类,一类是框架类的依赖(比如,每个APP总有自己的一些基础框架库),另一类是三方库(比如网络库,图片库等)。
    对于调用方而言,不管是第一种还是第二种,都是灾难性的。

    • 引入了一堆不必要的东西;
    • 必须追随引入模块的一套自定义规则、玩法;
    • 一个APP出现了多个网络库,多个图片库等。

    首要的方案,当然是拆拆拆,把不必要的功能模块拆出去;
    其次,可以通过接口依赖,依赖注入的方式来解决。  

  • 模块业务强耦合

    模块提供的能力与具体的一个业务产品逻辑有较强的绑定,不可灵活配置。
    这样的模块,必须重构,否则模块中的 if、else 会让后面的维护者看得眼花缭乱,胶水代码满天飞。

    • 首先从源头出发,业务领域是否抽象了?
      这个非常重要。如果源头无法抽象,那么技术下游就算抽象了,也会出现很多胶水代码。
    • 切忌面向过程编程,面向对象编程的一大好处便是,容易扩展,配置。
    • 技术设计过程中,注意模块内部子模块的划分,边界定义清晰。
  • 依赖树复杂

    需要牢记以下几个准则:

    • 上层模块可以依赖下层模块
    • 同一层级的模块应避免依赖
    • 严禁反向依赖
    • 尽量避免依赖单点
  • 基础底层依赖模块变动频繁

    如下图所示(虚线上部),假设 Module A为最底部模块,被 Module B、C、D依赖,Module E 依赖 Module B,Module F 依赖 Module C、D,在某次项目中,Moudle A 被改动了,从当前的依赖树来看(撇开模块向前兼容做得非常好),主工程依赖的这些模块 B、C、D、E、F都得升级。这个过程是非常无奈以及心虚的。

    怎么破?有几种方式:(下图虚线下部)

    • Module A 的拆分
      分析Moudle A,是否可以颗粒度变小,如虚线下部左图,拆分为 A1,A2。尽可能减少改动代码的影响面。比如,改动了A1,只需升级B、E模块。
    • Module A 模块接口与实现分离
      如虚线下部右图,模块A 接口和实现分离,接口只允许新增方法,不允许(避免)删除方法,所有其他上层模块依赖 Module A Base(接口),由主工程选择实现类。
      当Moudle A impl发生变动时,只需主工程升级版本即可,其余模块都不需要升级版本。
      接口模块里面包含哪些内容?通常是这个模块的能力接口以及模块的领域数据模型。

  • 模块边界模糊
    模块的权限没有收住,一旦开出去了,调用方随心所欲调,如下图虚线左图,这样会引来几个问题:

    • 模块内部的逻辑修改直接影响到调用方
    • 模块对外部调用不知情,无法判断自身模块内部的修改会给调用方带来什么影响
    • 口子不收敛,影响范围大

    通常模块内部是需要做好架构的,一个模块能提供什么能力出去,类似服务端的open api,这个api,你是可以外界调用的,但我模块内部的方法,数据结构都是不允许外界调用的。
    下图虚线左下图是个半成品,虽然提供了对外能力api,但没有收全,红色调用线是要严格禁止的。
    下图虚线右图是比较理想的状态,调用方只允许调用模块开放出来的api,其余不允许调用。

    比如,Android P的发布,google 制定了黑、灰、白名单,原则上对于hide的接口是不允许调用的,其实一方面也是从功能稳定、维护成本来考虑,系统升级,这些hide接口变化是会比较大的,会带来较多兼容性的问题。  

  • 资源冲突
    不可避免不同模块的开发工程师偶尔的“心有灵犀”,对于资源名称命名一样,最通常的做法便是,资源文件加前缀,图片资源需要自己加前缀。

    resoucePrefix $module_prefix
    
  • 重复依赖

    • 主工程仲裁版本,主工程exlude,模块provided依赖
    • 开源引入收敛,公共模块来承接
  • 模块内部结构混乱
    最好的方式就是详细设计,思考清楚,边界、分层。
    自顶向下的设计方式是一个不错的选择。清晰的类图,流程图等等都是一个优秀模块的基础。
    举例:下图是本地多媒体选择模块的示意类图,分层还是比较清晰。


六、健康模块的属性


  • 功能独立、聚合
  • 高可复用、高可维护、高可扩展
  • 低耦合、可插拔
  • 配置方便、灵活自定义
    主题的配置;功能的裁剪、组合
  • 向前兼容
  • 独立 Demo
    开发效率提升
  • 文档完善
    Readme的完整:包含不限于 模块的描述、模块的使用方式、模块提供的能力、模块版本的升级信息等等。


本文来自网易实践者社区,经作者韩坤芳授权发布