客户端模块化的精益求精
一、序
客户端的模块化、组件化的概念,一点都不陌生。每个公司、每个产品,无论大小或多或少都在做模块化相关的工作,甚至可以说,不做模块化,都不好意思说参加过企业级的项目。
到底是模块化,还是组件化?我个人觉得一种解释还是比较合理的:模块更偏于业务模块(比如订单模块、商品模块),组件更偏于技术类的模块(比如网络库组件、图片库组件),模块的颗粒度可能会更大一些,组件的颗粒度相对小一些。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的完整:包含不限于 模块的描述、模块的使用方式、模块提供的能力、模块版本的升级信息等等。
demo
相关文章:
【推荐】 HBase原理–所有Region切分的细节都在这里了