超过一年以上、活跃开发的项目往往到后期陷入了一些共性的问题:

  • 构建速度慢,往往生成一次最终输出产物需要一小时以上;
  • 架构复杂:虽然说架构本身可以用类似于MVC/Service Bus之类的通用进行描述,但实际上使架构变得复杂的往往是业务本身;
  • 开发速度慢,构建速度是因素之一,它使得持续集成的反馈大大低于预期;然而这类大的项目往往被通过各种技术手段进行了分层、分project的切割,你要面对的可能不是一个project,而是一组项目群。我之前参与过、咨询过的项目里,开发人员打开IDE要面对的project少则几十个,多则上百个。即便以目前最强劲的开发机器,面对这动辄几十万上百万行的代码,依然显得力不从心。
  • 以及由上面而引来的一系列问题:例如新人培养,知识传递等等

在提出这些问题的解决方案之前,我们看看这些问题是如何产生的。通常需要很长时间这些问题才成为问题,而且往往在一开始出现的时候,总有一些快速而有效的解决方案去掩盖,进而加剧了问题的升级,最终成为一个旷日持久需要大量人力才能解决的问题。

项目的产生

新的项目来了。团队成员兴奋的引入了最新的MVC技术框架(比如SpringMVC/ASP.NET MVC)、持久框架、依赖注入框架等等。现在流行的迭代开发方法也被引入。于是前几个迭代过去了,Domain, Service, Web等,分层良好的应用产生了。需求也快速的实现了。代码非常健康。构建速度非常快。所有人都很高兴。

2个月过去了。有心的团队成员不断的重构着代码,确保重复的逻辑、重复的代码被消除。新的人加入了团队。新的业务需求也来了。这些不断重构的代码进一步被不断重构着:终于引发了一些问题:由于只有一条主线:Domain -> DAO -> Service -> Web, 在并行开发下(比如同时有5-8个并行工作)那么公共使用的那条线会不断的产生代码合并冲突/或者业务逻辑冲突。

这不算一个多严重的问题。然而这个问题却制约了团队的规模扩张。比如需要更多的人加入这个项目的时候,耗费在沟通上的时间会大大增加,新加入的成员有效生产力也难以得到提升。

并不算太难解决的的问题。现在团队还不大。团队的架构角色只需要花上一个周末的时间,将现有的代码按照业务逻辑进行纵向切分,划分为不同的小项目,问题算是基本解决。

问题来了

更多的代码被提交。构建速度从2分钟上升到6分钟的时候有人抱怨了一下,于是花点时间优化了构建脚本,时间减少到5分钟。代码继续增长——这是不可避免的趋势——构建时间继续加长,从5分钟上升到11分钟的时候,大家的工作习惯开始发生了一些变化:一旦开始构建,就开始跟旁边的伙伴聊聊天,或者趁这机会喝点咖啡。本地提交在这个时候与持续集成服务器有点不同——本地可能只运行少量的构建步骤、必要的测试,服务器则运行所有的。

从11分钟上升到23分钟的时候,大家觉得要做点什么了。升级了所有开发人员的开发机器,最新的四核8G内存的机器,酷毙了。分布式构建集群也被引入。原来需要23分钟,现在通过分布式之后时间回落到10分钟以内了。

更多的问题

需求在不断的扩张着。代码的规模随之膨胀着。构建时间不引人注意的增长着。直到几年后的一天,突然发现:

  1. 即便已经使用分布式,构建需要一个小时

  2. 打开IDE面对的是72个项目

  3. 虽然能忍,但干什么都有点慢

  4. 架构呢?架构呢?

解决思路

大多数解决这类问题的思路仍然停留在表象层面:加机器(改善构建速度)、增强结对编程(改善交流)、写更多的Wiki(增加对代码的共识)。然而却逐渐忽略了一个事实,那就是:

这么庞大的“业务需求”,根本不是__一个__项目能够承载的。

让我们从代码层面开始。

一个大型项目需要在IDE里面打开数十个project. 这些project之间有着千丝万缕的联系——无论依赖被管理的多么好,没有人能够很清楚的知道他们之间如何被依赖的。更重要的是——大多数时间你都不会碰60%以上的project以及80%的代码。那么这些代码存在的意义何在?

因为你处在一个团队中,别人会用。

于是引用就成了依赖最强、最脆弱的代码引用。

那么,如果我们将这些项目的引用变成二进制引用呢(如JAR, DLL)?由于依赖的这些项目已经经过构建,那么编译的时间可以减少。你也只需要关注自己的项目。

听起来似乎太轻巧了。的确如此。如何获得这些二进制引用?对于JAR而言,假设一个Maven依赖仓库是必须的;对于DLL似乎没有太成熟的方案但总不是太难的问题。

这个过程之中有非常多的实现细节,很可能大多数团队在第一步:分析project依赖就跘住了脚。这么多的project想要拆开是很有挑战的事情,在业务需求的并行压力下,缺乏勇气的团队很可能止步于此。

这些依赖是组件吗?

在进行二进制引用的进程中,你应该不断的问自己这个问题:这个依赖是组件吗?还是只是一个简单的压缩包?

评估一个project是否为一个组件,在我看来有几个约束条件:

  1. 是否有超过2个project依赖于它?注意,这里的依赖,不是IDE里面你指定的依赖,而是真实的、API调用的依赖。对于组件化意识不好的团队,这类项目往往成为临时代码堆放地,需要通过识别、迁移,才能将真正有用的组件提取出来。

  2. 是否稳定?所谓稳定是指,在过去一段时间内(比如一个月),这个模块没有经过大的调整,API基本稳定,未来的变化只在于增加API的数量而非调整API的架构。

  3. 自己依赖于外面的足够少。

通过这一步,往往你可以识别出项目中用到的公共组件、公共API等等。将他们组件化,通过Maven或者自己的依赖库管理起来,标记上版本,然后所有人使用二进制引用。通过这一步,构建时间应当大幅度减少。通过这一过程的梳理,哪些是核心业务逻辑、哪些是可以独立考虑的第三方辅助库,应当可以有一个更为清晰的理解。更重要的是,这些组件可以独立开发、升级、优化,丝毫不会影响到主线的开发过程。

组件是库(Library)还是服务(Service)?

经过上面一步,可能项目中仍然存在一些项目依赖,这些项目往往是公共的,通过API调用的。例如,在某一个银行业务中,支付模块被很多其他业务依赖。支付模块有很多代码,也需要在主进程中与其他模块一起被部署。但支付模块实在是太独立了,虽然与其他的domain之间存在一些类上的简单交互。

采用上面步骤的方法不太合适,原因之一就是它是运行时才有效的依赖——它整体上是一个服务,而非一个静态的库。这个时候你可以考虑将其彻底独立,成为一个独立的service。它的形态可以是一个操作系统服务,或者独立部署的应用。然后写一组标准的轻量级API如REST/WebService来对其进行交互。这样这部分也独立出去了。

重要的考虑

上面看起来轻巧的过程实际上在操作过程中需要耗费很长很长很长的时间。原因之一是组件很难识别。然而难以识别的原因并非是这个过程很难,而在于我们在完成一个项目的过程中倾向于将所有的东西放到一起,顶多通过project区分但仍然缺乏真正物理意义上的隔离。这是一个认知上的障碍,特别是我们面对的是“项目”而不是“产品”。“项目”这个词本身就透露着短期的、目的性强的意义。识别出来的组件本身短期并不会给团队带来多大的好处,反而会增加工作量。就像所有的知识积累工作一样,它们的好处与他们的投入在因果关系上并不连续。

我们得到了什么?

这并非一个理想化的描述,最终我们得到的是:

  1. 真正物理隔绝的一组项目群:能够独立构建、开发部署和升级

  2. 依赖仓库

  3. 分工明确的组件和服务

  4. 针对产品的版本和部署策略

(完)

PS. 这里我所说的项目是指业务需求,project(英文)是指代码组织的一种形式例如eclipse/Visual Studio等IDE的“项目”。关于架构的术语之争从未停止过。这篇文章中大量用到了“框架”、“组件”、“库”、“服务”等词汇,也许跟你平时看到的不一样,如果有迷惑之处请谅解并指出。