作者:韩坤芳
客户端的模块化、组件化的概念,一点都不陌生。每个公司、每个产品,无论大小或多或少都在做模块化相关的工作,甚至可以说,不做模块化,都不好意思说参加过企业级的项目。
到底是模块化,还是组件化?我个人觉得一种解释还是比较合理的:模块更偏于业务模块(比如订单模块、商品模块),组件更偏于技术类的模块(比如网络库组件、图片库组件),模块的颗粒度可能会更大一些,组件的颗粒度相对小一些。Android 中对于多模块的开发,英文就叫 Module。咱们这篇文章里,姑且都叫模块吧。
模块化的路是长久的、持续的,不同的业务产品,实际需求可能会不太一样,咱们撇开特性,来说说模块化共性方面的一些探索与精益求精。
引用 Tim Berners-Lee 的一句话:
简单性和模块化是软件工程的基石;分布式和容错性是互联网的生命。
降低大型软件复杂性和耦合度
一个很庞大的系统,信息量之多会超过人脑的处理能力,进而失去分析能力。将庞大的系统通过业务、功能的边界划分,确定一个个子系统和组件,整个系统的架构一目了然。
模块重用、模块重组
避免重复造轮子,达到技术结果价值最大化。
微服务
成熟的软件架构思想
以下几个概念:OSGI、SPI、DI、微服务、CS 模式、事件机制等想必已经耳熟能详,不清楚的可自行 google。
客户端的模块化整体思路无非就是这些思想的汲取。
模块的梳理,依赖关系
技术模块的确定,首要的源头便是业务模块的划分和抽象。所以在做任何业务模块的之前,首先从需求的源头来圈住业务边界。模块颗粒度的控制,边界的确定。
模块需要分层,同一层级的模块应避免依赖,严禁反向依赖。
模块之间的通讯
一般 module 之间的通讯有以下几种:
模块注册,初始化
模块内部设计思想
本小节的大致思路是这样的:简述当前模块遇到的一些痛点,针对这些痛点,给出我们通常的解决方案,最后以一个功能模块作为实战案例来收尾。
以网易卡搭编程APP 为例,iOS 有 20+ 模块,Android 有 30+ 模块,模块的数量不算太多,但也不少。这些模块中,有些是教育产品部门几个产品(云课堂,中M,企业云等)共用的,有些是单独卡搭编程APP自己的。以这些模块作为样本,整理了下模块通常会遇到的一些痛点以及策略:
同一层级的模块通讯
同一层级的模块A,模块B,谁也不依赖谁,如何互相通讯。
如何解决?一般有以下几种方式:
模块全家桶模式
如果想要依赖一个模块A,然后就间接引入了模块A 依赖的模块 B,模块C,1 拖 N。
这边 1 拖 N 带入的模块,主要有两大类,一类是框架类的依赖(比如,每个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 模块接口与实现分离
如虚线下部右图,模块A 接口和实现分离,接口只允许新增方法,不允许(避免)删除方法,所有其他上层模块依赖 Module A Base(接口),由主工程选择实现类。
当Moudle A impl发生变动时,只需主工程升级版本即可,其余模块都不需要升级版本。
接口模块里面包含哪些内容?通常是这个模块的能力接口以及模块的领域数据模型。
模块边界模糊
模块的权限没有收住,一旦开出去了,调用方随心所欲调,如下图虚线左图,这样会引来几个问题:
通常模块内部是需要做好架构的,一个模块能提供什么能力出去,类似服务端的open api,这个api,你是可以外界调用的,但我模块内部的方法,数据结构都是不允许外界调用的。
下图虚线左下图是个半成品,虽然提供了对外能力api,但没有收全,红色调用线是要严格禁止的。
下图虚线右图是比较理想的状态,调用方只允许调用模块开放出来的api,其余不允许调用。
比如,Android P的发布,google 制定了黑、灰、白名单,原则上对于hide的接口是不允许调用的,其实一方面也是从功能稳定、维护成本来考虑,系统升级,这些hide接口变化是会比较大的,会带来较多兼容性的问题。
资源冲突
不可避免不同模块的开发工程师偶尔的“心有灵犀”,对于资源名称命名一样,最通常的做法便是,资源文件加前缀,图片资源需要自己加前缀。
resoucePrefix $module_prefix
重复依赖
模块内部结构混乱
最好的方式就是详细设计,思考清楚,边界、分层。
自顶向下的设计方式是一个不错的选择。清晰的类图,流程图等等都是一个优秀模块的基础。
举例:下图是本地多媒体选择模块的示意类图,分层还是比较清晰。
本文来自网易实践者社区,经作者韩坤芳授权发布