为什么要做模块化

我们都知道代码设计有个原则:

不要重复你自己。

代码要尽可能的被复用。对于同一个功能,当它第一次被写出来后,其他模块会去引用它。随着时间的推移,每个模块都有自己的一些功能点被其他模块调用,甚至作为基础的框架层还会去调用业务层的代码(比如需要拿到业务层对框架接口的实现、框架针对特定业务的需求对一些功能做定制)。

几个人小团队的项目里,这样做的问题倒也不是很大。但规模大点的项目都会遇到很多问题,比如

  • 持续集成构建时,自己的构建任务会因为其他人的代码而失败;
  • 部署服务时,生成的包非常大;
  • 启动服务时,常常因为奇怪的原因导致服务无法启动。
  • 其他情况,比如个别业务新功能无法单独上线,一切都要跟着整体节奏。

好处

  1. 工程师学习成本大幅降低,特别是刚加入团队的新人。未模块化的代码,往往调用、依赖关系混乱、边界不清晰。模块化之后工程师可主要查看、修改本模块代码。
  2. 测试的难度降低,也更加具有可行性。因为有了清晰的边界,开发人员可以从单元、模块角度对相关功能施加测试。
  3. 持续集成速度加快,在一些持续集成中会做正向依赖检查和反向依赖检查,相当耗时,理清依赖后,只需负责自己相关上下游的测试就行了。
  4. 持续集成成功率正常可以从50%提升到95%以上,耗时从数分钟缩短为几十秒。
  5. 模块化之后,业务可以单独部署。如果再加以改进,还可以向微服务更进一步。

怎么做

  1. 依赖接口,而不是依赖实现。接口只能依赖于接口或者简单的bean。接口是对外的一个约定,往往比较稳定。而且接口对其他模块的依赖往往比较少,实现一般频繁变更而且有较多对外依赖。当一个模块对其他模块的依赖仅限于接口,那么后期的可维护性会大大提高。接口实例的创建往往不需要调用者关心,而是通过工厂或者依赖注入的形式在真正用到的地方创建出来。
  2. 如果两个模块或者多个模块之间存在循环依赖,就需要做依赖下移。举个例子,如果A和B相互依赖,那需要把B中A依赖的代码抽出来到另一个模块C,这样A、B都依赖C而A不再依赖B,把A中B依赖的代码抽出来到另一个模块D,这样A、B都依赖D而B不再依赖A。
  3. 定义好抽象层次。基础平台的公共代码往往比较通用,比如做序列化、反序列化,发布和订阅事件。这些往往是最下层的。基础平台的代码最好有统一的版本号管理,比如Spring一次发布出来各个模块都是同一个版本号,这样上层代码对基础平台的依赖不会出现版本不一样导致的问题。中间层业务模块可能也会有一些公共的部分,其他模块也会调用,将这些公共代码下沉到应用核心库中,这样其他业务可以调用。但是这不是长久之计。
  4. 如果服务规模比较大,模块比较多,可以考虑将其微服务化。需要定义好每个业务的边界,建立模型。