都说应用大了就要拆,有人建议拆成多个应用并构成分布式系统,有人建议维持一个应用但拆成多个包(指jar包,不是package);本文讨论的是后一种分拆法。
下面从多个方面来讲述分包的好处。我将主要使用“反证法”,论述大应用如果不分包可能导致的后果,反过来证明分包的合法性。 本文的观点和论据大部分来自于Robert C. Martin的《敏捷软件开发》,有兴趣的朋友可以细阅这本书的第20章-“包的设计原则”
从软件开发的多个方面看待分包问题:
系统的可理解性
分包可以提供模块化的视图,帮助了解系统的组织结构
如果一个大公司没有分部门,你是不是很难说出这家公司哪些人有哪些职能? 如果一个系统的java代码都堆在一起,你是不是也很难说清这个系统大概有哪几块? 如果系统的结构不清晰,对大家理解系统、系统架构的未来规划都有很大的坏处。
而系统分包就是系统模块化的过程;结合包依赖图,可以帮助了解系统的组成部分以及模块之间的依赖关系
编译
a. 分包可以避免一次编译所有代码,减少编译时间
如果代码不分包,则意味着每次编译都要编译整个应用;对于一个大的应用,这个编译时间会很长;如果应用是c/c++写的,这个编译时间可能会达到无可容忍的地步。
分包之后,大部分代码已经编译掉、作为jar文件引入了,此时你要编译的代码只是你正在改的那一小部分。这种情况下,编译时间可大大缩短。
b. 分包后,服务提供者的接口变更不会立即导致消费者代码编译失败
如果代码不分包,且大家都在同一份代码库里开发,会有这样一种场景:A写完FooService准备提交时,发现它依赖的BarService已经被删掉了! 这时A就没办法提交代码了。如果A是一个冒进的人,他在提交之前不会更新代码库,而是直接提交FooService,然后下班;比他下班晚的人如果签出最新代码,就会发现代码无法编译。
如果大家各搞一个私有分支,可以避免这种问题;大家在各自的分支里进行开发,最后再合到一起。但最后合并的工作量可能很多,因为要解决的代码冲突可能会很多或者很难搞,上面说的BarService被删掉的问题就很难搞;应用越大,这种冲突的概率就越高。
把应用拆成带版本的jar包可以解决这个问题:把BarService放到bar-1.0.jar里,FooService依赖bar-1.0.jar. 如果BarService被删掉,删它的人会打出一个bar-1.1.jar; FooService则仍然使用bar-1.0.jar,不会出现编译问题。
3.代码的正确性
分包后,服务消费者不必使用服务提供者的最新版本,避免出现未经验证的代码组合
如果大家都在同一份代码库里开发,A写完FooService自测并提交,然后B把它依赖的BarService改掉,FooService和新的BarService共同运行时可能就会导致错误的结果,因为对于这个Foo+Bar组合,还没人测过。
把应用拆成带版本的jar包可以解决这个问题:把BarService放到bar-1.0.jar里,FooService依赖bar-1.0.jar. 如果BarService被修改,修改它的人会打出一个bar-1.1.jar; FooService依赖的仍然是bar-1.0.jar,不会出错。
4.代码的安全性
分包后,每个包独占一个代码库,方便用作权限控制的单位
有些代码只准某些人修改,这种控制一般要依托版本控制系统(VCS)实现。 如果整个应用的代码都放一起,想对某部分代码做权限控制,意味着要对代码库里的某些子目录进行权限控制,VCS在这方面的支持往往不好,或者配置起来不那么简单。把应用拆成多个jar包后,每个jar包的代码库对VCS来说都是一个独立的代码库,这时再做权限控制会容易的多。
5.架构
分包体现了防卫性设计,可以保证层次之间及模块之间的单向依赖
如果整个应用的代码都放在一起,难保不会有人用DAO调Service, Service调Servlet; 也难保不会有人让一个基础应用调用一个易变应用,比如UserService里调用GoVocationBiz
把代码分成包后,通过maven/ivy等包际依赖管理工具,可以明确定义好包之间的依赖关系,避免上述事情发生; 如果开发者试图让user-service.jar依赖了vocation-biz.jar,他会在review时被阻止,因为这种级别的代码变动往往需要团队里的资深成员介入review
6.项目进度
分包后,服务提供者的升级项目和消费者的升级项目可以错开进行
整个应用的代码放在一起时,如果BarService的接口改了,依赖它的FooService也得跟着改;当BarService发布时,FooService也得跟着发布,即使FooService团队正在忙于其它的项目,也不得不抽出时间内重构FooService并盯着它和BarService一起发布; 如若不然,则BarService只好搁置自己的升级计划。
有的团队会把BarService打成包(出于防卫性设计考虑),却不使用版本机制;在这种情况下,FooService总是使用BarService的唯一版本,也就是BarService的最新版本,此时,进度捆绑问题仍会发生。
如果把代码分包并引入版本机制,当BarService升级后,FooService仍可以使用老的BarService版本继续运行;等FooService团队有空了,再另开项目使FooService适应BarService的新版本。 BarService的升级项目和FooService的升级可以分开做