分类: 项目管理
2012-02-16 15:45:35
分布式开发
闭门造车的软件开发时代早已过去。在嵌入式系统之外,几乎每一位开发者都需要依赖别人写的类库或框架。这种借助并复用他人提供的基础设施、框架以及类库的好处在于使自己能够专注于应用本身的逻辑当中。这样缩短了软件开发所需要的时间。
过去的几十年间,开源软件的兴起令类库的复用具有双倍的吸引力。我们现在有针对多种程序中的各种问题而诞生的现成解决方案,而获取这些解决方案不用花一文钱。开源产品起于UNIX内核,基础C类库和命令行工具,并通过Web服务器和Web浏览器延伸至Ant,Tomcat,JUnit,Javacc等Java工具领域——而这种情况还有无限制发展的趋势。在编写一个现代软件的过程中,集成工作的部分和创新的部分大致是对半分的。将可用的碎片捡起来并组合到一起是现代应用开发的主要工序。人们不再从零开始编写一切。人们在需要HTTP服务器的时候为他们的应用选择Apache或者Tomcat,在需要数据库的时候选择MySQL或PostgreSQL。应用软件将这些零碎部件粘连起来,并加入自己的逻辑。最终的成品是功能完备的、性能好的、并且在相当短的时间内开发出来的应用软件。
看看Linux版本是如何发行的。红帽的Fedora,Mandriva,SUSE,还有Debian,它们所包含的应用程序其实大致上差不多,而且都是同一群人写的。发布者不过是简单的将它们打包,并提供“胶水”用于统一的安装。发行商往往只编写中央管理软件和安装软件,并提供一些质量保证,以确保所有选定的组件能够协同工作。这个过程对于Linux的普及产生了相当理想的效果。有一个例子可以证明此模型的意义,那就是Mac OS X:它其实就是个安装了一堆苹果插件的FreeBSD Unix。对于这样的软件,需要注意的一个重点就是它创建的方式采用了一种分布式开发模型。软件的开发者和发行者可能完全不认识对方,也没有交流过,而他们往往也并不生活在同一个地域。
这种分布式开发有如下特征。第一,应用程序(或操作系统)的源代码不再处于某一个开发者完全的掌控之中。源代码被散布至世界各地。毫无疑问,构建这样的软件与传统那种源代码完全在你家中的代码库的应用构建是完全不同的。
另外我们需要了解的是,也没有一个人对整个项目的时间表有完全的掌控。不单单是源代码,开发者们也遍布世界各地,并以他们自己的时间表工作。这种情况并不像听起来的那样不寻常或不靠谱。如果你曾经为超过五十人的项目制定过时间表,那么你一定会明白,对整个项目进程拥有“完全的掌控”最多只是一个安慰自己的幻想。你随时需要准备好抛弃某个特性,或是发布这个或那个组件的一个老版本。同样的模式也适用于分布式开发。
每个人都有这样一个权利:使用一个新版本或旧版本类库的自由。
使用外部库并使用它们组建应用程序,这意味着人们能够花费更少的时间和精力创造更复杂的软件。代价则是,我们需要管理这些类库,确保它们的兼容性。这不是一个简单的任务。但是,对于如今高度复杂系统的组建,也没有其他既实用、性价比又高的开发模式了。
模块化应用程序
针对分布式开发的挑战,其技术解决方案就是模块化。在一大块紧密耦合的代码中,每个单元都可能与其他单元进行直接的接口。而模块化应用则正相反,它由小块的、分散的代码块组成,每一块都是独立的。于是,这些代码块可以由不同的团队进行开发,而他们都有各自的生命周期和时间表。最终的成果则可以由另一个独立的个体,即发行者,进行集成。
对于Java而言,将一组类库放在Java类路径上并运行一个应用程序在很早以前就实现了。NetBeans平台在类库的管理方面已经走的相当远:它积极的参与类库的加载过程,并强制每一个类库都满足其他类库对自己的最低版本需求。这样的类库被称为模块。NetBeans模块系统是一个运行时容器,它确保了系统在运行时的完整性。
版本控制
将应用程序分解为独立的类库,这带来了一个新的挑战——我们需要确保这些互不依赖的零件们能够在一起工作。这个问题有多种解决方案,而最流行的一种就是版本控制。每一块模块化应用都有一个版本号,常用杜威十进制格式表示,比如1.34.8这种数字组合。新版本的发布带来增加的版本号,比如1.34.10,1.35.1,或者2.0。其实仔细想来,使用增长的版本号来代表两个版本的复杂软件之间的不同是挺荒谬的。不过这种方法解释起来很简单,而且它的流行也说明了这种方法是十分可行的。
一个模块系统的另一个特点是外部依赖的声明。很多组件对外部条件有一定需求。比如说,一个模块系统中的组件可能需要一个XML解析器,或者需要安装某种数据库驱动,或者需要某种文本编辑器或者浏览器才能工作。对于每一个需求,另外一个模块可以指定其接口的特定版本号。即使对外部类库的依赖性极低,但每一个Java程序都对Java本身有版本要求。一个真正的模块系统可以指定理想的最低JDK版本。一个模块可能会有JDK>=1.5,xmlparser>=3.0,webbrowser>=1.5这样的版本需求。在运行时,启动应用的模块代码的依赖条件需要被满足,即,XML解析器在3.0版或以上,浏览器在1.5版或以上,如此这般。NetBeans模块系统正是这样的。使用依赖模式来维持模块系统中组件之间的依赖性有一个大前提,那就是我们必须遵循一系列的规则。第一个规则是向后兼容性:如果新版本发布,那么所有在之前版本下可建立的契约也必须能够在新版本下工作。这一点说起来很容易,但实现起来没那么容易。第二个规则是,系统中的组件需要准确的说明它们需要什么。当一个模块的依赖性产生改变的时候,它必须要说出来,这样系统才能够准确的确认这些依赖性是否被满足。因此,如果一个模块系统产生了对新功能的依赖性,比如一个HTML编辑器,那么你便需要定义这个新的依赖性(比如,htmleditor>=1.0)。同时如果你开始使用一个新的HTML编辑器组件的接口,而这个接口在1.7版之后才有,那么你需要更新你的依赖型需求到这个组件的1.7版本:htmleditor>=1.7。在NetBeans模块系统中,第二个规则在实践当中是相对容易遵循的,因为一个模块的编译时类路径仅仅包括有依赖性声明的模块,而没有依赖性声明的模块是不会被编译的。
二级版本信息
之前我们有关版本控制方法的讨论针对的是类库的规范版本。规范版本描述了该类库当中的公共API的一个特定快照。
某些版本的类库会不可避免的遭遇不得不修复的bug。因此,二级版本的识别也应该与组件关联起来,那就是这个组件的实现版本。与规范版本不同,一个实现版本往往用“Build20050611”这样的字符串进行标注,因此只能通过等式来判定。这就提供了一个二级识别机制,这个机制可以用来决定某个特定的代码模块是否有必须修复的bug。我们知道,在3.1规范版本中存在的bug未必会在3.2版本或者3.1版本的其他实现版本中存在,因此,出于bug修复或某些特殊处理的需求,将实现版本与类库关联起来是十分有用的。
依赖性管理
版本和依赖性系统需要一个管理器,以确保这个系统中的每一部分的需求都得到满足。这样的一个管理器可以检查每一块组件的安装时间,保持系统的一致性——Linux发行版的RPM或Debian包就是这样工作的。描述依赖性的元数据在运行时非常重要。有些元数据可以让应用进行动态类库升级而无需关闭应用。元数据还能决定一个模块动态加载的依赖性是否满足。如果没有满足,元数据将向用户解释可能会遇到的问题。
NetBeans IDE是一个模块化应用。它的模块——即组成它的那些类库——在运行时被查知并加载。它们可以安装小块小块的功能,如组件,菜单项或服务等;它们可以在启动时运行代码,进行程序初始化;它们可以通过声明式注册机制把平台和IDE提供的各个部分注册为服务并在需要时将其初始化。NetBeans模块系统使用安装组件的声明依赖性为每个模块的类路径进行父类路径配置,并在模块加载类的时候决定在哪些JAR文件当中搜索。这样确保了模块类路径当中不存在任何不属于其依赖树的模块JAR,并强制确保每个组件都有声明式依赖性。一个没有声明依赖性的模块将无法从其他模块中呼叫代码,而当依赖性没有被全部满足时,其他模块将不会加载。