很多开发团队通常严重依赖于版本控制系统的分支功能。分布式版本控制系统让分支操作更加方便。然而,在《持续交付》一书中描述的很多非常规言论中,就有一条是:“使用分支,你就无法做持续集成”。根据定义,如果你有代码在某个分支上,那就没有集成。有一种很常见的情况,会让人很自然地想到利用版本控制工具的分支功能:那就是“对应用程序进行大规模改造时”。然而,还有一种替代这种真实分支的做法,技术上叫做“抽象分支(Branch by Abstraction)”。
抽象分支:在主干上进行以增量方式对软件进行大规模改造的一种模式。
Paul Hammant在2007年就提到过用这种方式把OR mapping的方案从Hibernate切换到iBatis(详见这里)。同样,我曾经工作的一款商业产品(持续集成与敏捷发布管理平台 Go)则从iBatis改到了Hibernate,这已经是两年前做的事情了。我们也把产品的UI层慢慢地从“使用Velocity和JsTemplate”转移到了“JRuby on Rails”上。
这两种变化都是慢慢地增量式地完成的,在改变的同时,也做新功能的开发,但这并不妨碍我们每天向Mercurail的版本库主干上提交数次代码,甚至在切换过程中做了数次正式发布。我们是如何做到的呢?
从iBatis迁移到Hibernate
团队决定从iBatis迁移到Hibernate,有两个原因:第一,我们可以更高效地使用ORM,因为我们对产品数据库的结构有绝对控制权,这样就不用写太多的定制SQL;第二,它有二级缓存,对性能有帮助。
当然,我们并没想一次性把整个代码库都迁移到Hibernate上。我们的策略是:当开始增加新功能时,如果需要增加新的方法去访问数据库的话,就使用Hibernate来完成,当必要时,才将原有对iBatis的调用迁移过来。
对持久层逻辑的更新相对来说比较直接,因为产品 Go的代码库使用标准的分层结构,控制器层使用服务层,而服务层使用仓库层(repositories)。因为所有需要访问数据库的代码都利用repository pattern封装在仓库层中,所以每次将一个仓库从iBatis改成Hibernate,增量式地完成修改是一件比较容易的事情。服务层根本不知道底下的持久层框架是什么。
我 的同事Pavan K.S.说:“抽象分支有一个严格要求,那是纪律性,即:开发人员不能再以任何借口添加原有模式的代码。也就是说,作为第一原则,不要再增加iBatis查询(尽管这么做可能更快更省事儿),必须用Hibernate来做。这是确保你进度的唯一方法。一种强制手段是在持续集成构建时只要发现新增了iBatis查询, 就令持续集成构建失败。并且,只能不断减少这个阀值,绝不能增加。”
从Velocity和JsTemplate转向JRuby on Rails
产品GO还从“以Java为基础的UI软件栈”转向“JRuby on Rails”的软件栈。这也有两个原因:一是新的框架更容易写测试,二是它会加速UI的开发。当然,这个变更也是增量式完成的。当在应用程序中创建新的页面时,我们会使用JRuby on Rails,一旦做好以后,就让应用程序的其它部分指向这个新页面。
当需要对某个旧页面进行大量变更时,我们就把它迁移到JRuby on Rails上。一旦做好,就把应用程序中所有指向这个页面的URI都更改为这个新的页面。此时,要把对应的旧页面删除。所以,当Go的界面大部分都是JRuby on Rails的实现时,仍旧有一些页面是原有JAVA版的实现。
然而,只看页面的话,根本不会觉察,因为它们的样式是统一的,但从URI是能够看出来的。所有使用/go/tab前缀的URI都会跳转到旧的Velocity页面上。其它页面会跳转到JRuby on Rails页面上,当然它也同样会使用原有界面所用的java 服务层。
抽象分支究竟如何操作呢?
抽象分支通过如下几个步骤进行大规模增量式修改:
在你想改变的那部分代码之上创建一个抽象层。
对其余部分的代码进行重构,使其使用这个抽象层使用其之下的代码提供的功能。
在新的实现代码里实现一些新的类,让其上的抽象层根据需要,选择性的导向旧代码或新增的类上。
剔除原有的旧实现。
清理,并重复前两步,如果需要,可同时交付你的软件。
一旦旧实现完全被代替后,如果你愿意,可以移除那个抽象层。
老马(Martin Fowler)指出,这些步骤也可以变化一下。“在最简单的情况下,你可以创建一个抽象层,然后重构,让所有的代码都调用它,然后再新写一个实现,最后切换一下就行了。但是,还可以将它分开做。比如,不创建整个抽象层,而只是创建将要修改的功能的一个子集,迁移这部分代码,然后再做下一部分(此时新旧代码共存)。”
在上面iBatis/Hibernate的例子中,抽象层就是指那个仓库层,它隐藏了持久层框架使用的细节。在JRuby on Rails的例子中,抽象层是Servlet Engine,通过URI的匹配,它可以决定是将Request分发到JRuby on Rails框架,还是标准的Java Servlets上。
尽管Go这个项目相对比较小,开发人员不到十个,而且到现在也仅有五年的时间,但是,这些原则完全可以应用于各种大小的项目上。即使在大型且分布式的团队项目里,也可以成功地使用这种模式。
不可否认的是,抽象分支在开发过程上增加了开销,而且当你的代码库结构性很差时,开销会更多一些。为了能够以这种方式做增量式变更,你必须仔细思考,一点儿一点儿地慢慢向前走。但是,在很多情况下,这种额外的工作量是值得的,越是大的重构,就越应该考虑使用这种抽象分支。
抽象分支的关键收益是你的代码在整个结构调整的过程中都能够正常工作,能够做到持续交付。也就是说,你的发布计划与架构上的调整完全解耦,因此,在任何时间点你都可以停止重构工作,做优先级更高的事情,比如发布一个你刚刚想到的非常好的功能特性。
对于抽象分支来说,需要定义一个终止策略,这一点非常重要。当你能够做到“不完成全部的结构调整也可以发布”时,很容易产生一种倾向,即:一旦完成了重要部分的改造后,剩下的那部分尚未完成的工作,就放在那里不管了。然而,在系统中混合多种技术会让系统更难维护,也要求团队非常了解哪些地方还在使用旧有技术实现。这也许是一种可接收的权衡状态,但至少要对整个团队做到可见。
原文转自:http://www.continuousdelivery.info/index.php/2013/01/04/branch_by_abstraction/