Java应用架构设计:模块化模式与OSGi

更多详情


内容简介: 《Java应用架构设计:模块化模式与OSGi》由全球资深专家撰写,世界级软件开发大师Robert C. Martin与Peter Kriens作序推荐。书中揭示了模块化的重要性,如何实现模块化,以及如何使用OSGi实现模块化架构。
《Java应用架构设计:模块化模式与OSGi》分为三部分。第一部分(第1~7章)描述了需要模块化的理由。在这里,你将会看到模块化在设计软件中所扮演的重要角色,与此同时还会学习到为什么需要设计模块化的软件。第二部分(第8~12章)介绍了18个模式,这些模式会帮助你设计更为模块化的软件。第三部分(第13~17章)引入了OSGi,然后阐述了如何使用本书中的模式来设计软件系统并充分发挥模块化平台的优势,并使用代码样例论证了我们的观点。

目录: 《Java应用架构设计:模块化模式与OSGi》
本书赞誉
译者序
序1
序2
前言
第一部分 模块化的理由
第1章 模块定义 / 2
1.1 定义模块 / 2
1.1.1 可部署 / 3
1.1.2 可管理 / 3
1.1.3 可测试 / 3
1.1.4 原生可重用 / 3
1.1.5 可组合 / 4
1.1.6 无状态 / 4
1.2 软件模块的简洁定义 / 4
1.3 结论 / 4
第2章 模块化的两个方面 / 5
2.1 运行时模型 / 5
2.2 开发模型 / 5
2.2.1 编程模型 / 6
2.2.2 设计范式 / 6
2.3 模块化现状 / 8
2.4 结论 / 10
第3章 架构与模块化 / 11
3.1 定义架构 / 11
3.2 关于软件架构的一个故事 / 12
3.2.1 象牙塔 / 12
3.2.2 乌龟和塔 / 13
3.3 架构的目标 / 15
3.3.1 悖论 / 15
3.3.2 消除架构 / 16
3.4 模块化:被忽视的部分 / 17
3.5 回答我们的问题 / 23
3.6 结论 / 23
3.7 参考文献 / 24
第4章 征服复杂性 / 25
4.1 企业级复杂性 / 25
4.2 技术债 / 26
4.3 设计腐化 / 27
4.3.1 干扰可维护性 / 27
4.3.2 阻止可扩展性 / 28
4.3.3 抑制可重用性 / 28
4.3.4 限制可测试性 / 28
4.3.5 妨碍集成 / 28
4.3.6 阻碍理解 / 29
4.4 循环依赖 / 29
4.4.1 循环类型 / 29
4.4.2 悄然引入的循环 / 32
4.4.3 管理循环 / 33
4.4.4 循环总是不好的吗 / 33
4.5 结合点、模块和SOLID / 34
4.6 管理复杂性 / 35
4.7 模块化的益处 / 37
4.8 结论 / 37
4.9 参考文献 / 38
第5章 实现重用 / 39
5.1 可用/重用悖论 / 39
5.2 关于重用的免责声明 / 40
5.2.1 粒度 / 40
5.2.2 重量级 / 41
5.3 重用还是可用 / 42
5.4 模块化权衡 / 42
5.5 模块化设计 / 43
5.6 结论 / 44
5.7 参考文献 / 45
第6章 模块化与SOA / 46
6.1 重新审视“自上而下” / 46
6.2 粒度——架构师的强大对手 / 48
6.2.1 现实世界的一个例子 / 48
6.2.2 提升一个等级 / 49
6.2.3 另一个维度 / 50
6.2.4 全景图 / 51
6.2.5 服务样例 / 52
6.3 另一个视图 / 54
6.4 结论 / 55
第7章 参考实现 / 56
7.1 为什么不用OSGi / 56
7.2 这个练习的背景:构建系统 / 57
7.3 初始版本 / 57
7.4 第一次重构 / 59
7.5 第二次重构 / 61
7.6 第三次重构 / 63
7.7 第四次重构 / 66
7.7.1 关于OSGi的好处 / 67
7.7.2 小结并准备下一次重构 / 68
7.8 第五次重构 / 68
7.9 第六次重构 / 69
7.10 第七次重构 / 72
7.11 事后剖析 / 72
7.11.1 关于模块测试 / 74
7.11.2 关于管理模块依赖 / 74
7.11.3 关于模块重用 / 76
7.11.4 关于构建 / 77
7.11.5 关于面向对象 / 77
7.12 结论 / 78
7.13 参考文献 / 78
第二部分 模 式
第8章 基本模式 / 81
8.1 管理关系 / 81
8.1.1 表述 / 81
8.1.2 描述 / 81
8.1.3 多种实现 / 82
8.1.4 影响 / 85
8.1.5 样例 / 86
8.1.6 小结 / 88
8.2 模块重用 / 89
8.2.1 表述 / 89
8.2.2 描述 / 89
8.2.3 多种实现 / 91
8.2.4 效果 / 92
8.2.5 样例 / 93
8.2.6 小结 / 99
8.3 模块内聚 / 99
8.3.1 表述 / 99
8.3.2 描述 / 100
8.3.3 多种实现 / 100
8.3.4 效果 / 101
8.3.5 样例 / 101
8.3.6 小结 / 104
第9章 依赖模式 / 105
9.1 非循环关系 / 105
9.1.1 表述 / 105
9.1.2 描述 / 105
9.1.3 多种实现 / 106
9.1.4 效果 / 107
9.1.5 样例 / 107
9.1.6 小结 / 113
9.2 等级化模块 / 113
9.2.1 表述 / 113
9.2.2 描述 / 113
9.2.3 多种实现 / 115
9.2.4 效果 / 115
9.2.5 样例 / 116
9.2.6 小结 / 117
9.3 物理分层 / 118
9.3.1 表述 / 118
9.3.2 描述 / 118
9.3.3 多种实现 / 119
9.3.4 效果 / 119
9.3.5 样例 / 120
9.3.6 小结 / 123
9.4 容器独立 / 124
9.4.1 表述 / 124
9.4.2 描述 / 124
9.4.3 多种实现 / 125
9.4.4 效果 / 125
9.4.5 样例 / 126
9.4.6 小结 / 129
9.5 独立部署 / 129
9.5.1 表述 / 129
9.5.2 描述 / 130
9.5.3 多种实现 / 130
9.5.4 效果 / 131
9.5.5 样例 / 132
9.5.6 小结 / 135
9.6 参考文献 / 136
第10章 可用性模式 / 137
10.1 发布接口 / 137
10.1.1 表述 / 137
10.1.2 描述 / 137
10.1.3 多种实现 / 138
10.1.4 效果 / 140
10.1.5 样例 / 141
10.1.6 小结 / 146
10.2 外部配置 / 147
10.2.1 表述 / 147
10.2.2 描述 / 147
10.2.3 多种实现 / 147
10.2.4 效果 / 148
10.2.5 样例 / 149
10.2.6 小结 / 151
10.3 默认实现 / 151
10.3.1 表述 / 151
10.3.2 描述 / 151
10.3.3 多种实现 / 151
10.3.4 效果 / 153
10.3.5 样例 / 153
10.3.6 小结 / 156
10.4 模块门面 / 156
10.4.1 表述 / 156
10.4.2 描述 / 156
10.4.3 多种实现 / 156
10.4.4 效果 / 158
10.4.5 样例 / 158
10.4.6 小结 / 162
第11章 扩展性模式 / 163
11.1 抽象化模块 / 163
11.1.1 表述 / 163
11.1.2 描述 / 163
11.1.3 多种实现 / 164
11.1.4 效果 / 165
11.1.5 样例 / 165
11.1.6 小结 / 168
11.2 实现工厂 / 168
11.2.1 表述 / 168
11.2.2 描述 / 169
11.2.3 多种实现 / 169
11.2.4 效果 / 171
11.2.5 样例 / 171
11.2.6 小结 / 175
11.3 分离抽象 / 175
11.3.1 表述 / 175
11.3.2 描述 / 175
11.3.3 多种实现 / 175
11.3.4 效果 / 178
11.3.5 样例 / 178
11.3.6 小结 / 180
11.4 参考文献 / 181
第12章 通用模式 / 182
12.1 就近异常 / 182
12.1.1 表述 / 182
12.1.2 描述 / 182
12.1.3 多种实现 / 182
12.1.4 效果 / 183
12.1.5 样例 / 184
12.1.6 小结 / 187
12.2 等级化构建 / 187
12.2.1 表述 / 187
12.2.2 描述 / 187
12.2.3 多种实现 / 189
12.2.4 效果 / 190
12.2.5 样例 / 191
12.2.6 小结 / 195
12.3 测试模块 / 195
12.3.1 表述 / 195
12.3.2 描述 / 195
12.3.3 多种实现 / 196
12.3.4 效果 / 197
12.3.5 样例 / 198
12.3.6 小结 / 201
第三部分 模块化架构模式与OSGi
第13章 OSGi简介 / 204
13.1 一点历史 / 204
13.2 OSGi所能带来的收益 / 204
13.2.1 模块化开发 / 205
13.2.2 管理依赖 / 205
13.2.3 模块平台 / 205
13.2.4 版本化的bundle / 206
13.2.5 动态(重)部署 / 206
13.2.6 环境相关的控制 / 206
13.3 深入理解OSGi / 206
13.4 OSGi bundle / 207
13.4.1 bundle状态 / 207
13.4.2 OSGi ?Service / 208
13.5 OSGi运行时管理 / 209
13.6 重新查看模块化的两个方面 / 209
13.7 OSGi与模式 / 209
13.7.1 管理依赖 / 210
13.7.2 动态性 / 210
13.7.3 Blueprint规范 / 211
第14章 贷款样例与OSGi / 212
14.1 起步 / 212
14.2 清单文件 / 214
14.3 ?Service / 215
14.3.1 Blueprint服务 / 215
14.3.2 贷款样例配置 / 215
14.3.3 OSGi ?Service声明 / 218
14.4 安装与执行 / 219
14.5 结论 / 220
第15章 OSGi与Scala / 221
15.1 起步 / 221
15.2 Scala代码 / 221
15.3 Scala bean配置 / 224
15.4 Scala ?Service配置 / 224
15.5 构建Scala模块 / 225
15.6 安装与执行 / 225
15.7 结论 / 226
第16章 OSGi与Groovy / 227
16.1 起步 / 227
16.2 Groovy代码 / 227
16.3 Groovy bean配置 / 230
16.4 Groovy ?Service配置 / 230
16.5 构建Groovy模块 / 230
16.6 安装与执行 / 231
16.7 结论 / 232
第17章 OSGi的未来 / 233
17.1 将OSGi作为推动者 / 233
17.2 颠覆性 / 234
17.3 生态系统的威力 / 236
17.3.1 生态系统与模块化的两个方面 / 236
17.3.2 基于组件的开发(CBD)不是已经成功了吗 / 236
17.4 生态系统 / 237
17.5 结论 / 238
附录A 类设计的SOLID原则 / 239

译者序: “分而治之”是解决复杂问题的有效方式。
面对业务功能复杂的企业级软件,我们会寻找各种方式和标准进行拆分,其目的无非是降低每一部分的复杂性并提高软件重用的便利性。但是,到目前为止,取得的效果并不理想。应用程序依然是庞然大物,难以进行维护和管理,而在重用方面,最常见的方式恐怕还是复制和粘贴。各方面似乎都不尽如人意。
在本书中,作者提供了解决这类问题的另一种方案,那就是模块化。借助模块化技术,我们可以提升软件的架构水平,填补架构师和开发人员在相互理解上的鸿沟,同时又能提升软件的可重用性,控制软件的复杂性。目前,在Java平台中,OSGi是事实上的模块化标准。Java原生的模块化实现Jigsaw已经从Java SE 7延期至Java SE 8,最近又延期至Java SE 9,而且Jigsaw能否得到其他JDK厂商的支持还有待观察。换句话说,Java平台上的模块化技术还在不断发展中,但是本书介绍的模式和理念却具有通用性,虽然在书名上你可以看到OSGi的字眼,但是作者在介绍这些模式的时候,却在有意地与特定技术保持距离。只要你对系统进行了良好的设计和拆分,不管使用什么模块化技术,甚至没有模块化运行框架,你都会从中受益。
OSGi技术在嵌入式领域取得了很大的成功,目前它越来越多地用于企业级应用服务器上,但是在企业级软件开发领域,它的使用并不广泛。这是因为Java EE和OSGi在诞生之初就是不同应用领域的两种技术,所以二者在理念上以及使用方式上都会有很多不兼容或冲突的地方。但是,这种现象正在发生着变化,随着OSGi企业级规范的不断完善,以及像Apache Aries和Eclipse Gemini这些参考实现的成熟,相信Java EE和OSGi之间的壁垒会逐渐打破,模块化在企业级软件开发中的发展值得期待。在本书中,有众多设计模块化软件的最佳实践,相信随着模块化技术的不断发展,它会越来越有价值。
在翻译本书的过程中,作者深厚的技术功底和广泛的知识涉猎都令我佩服,尤其是借此机会,更认识到Bob大叔那几本名著的价值。这些书值得我们一遍遍地仔细研读,在此推荐给大家。
感谢侯伯薇向我介绍了这本书,并将我引荐给出版社。在本书的翻译过程中,编辑关敏给予了很多的帮助和指导,她的热心和责任心让我很受感染,在此向她表示感谢。
在此,感谢我的家人,没有你们的支持,我很难把这项任务坚持完成,尤其是我的爱人和宝贝儿子。很抱歉在最近几个月中,没有抽出太多的时间陪伴你们。
在翻译的过程中,我尽可能做到准确,但肯定还会有纰漏之处,恳请读者朋友们批评指正,您可以通过电子邮件:levinzhang1981@gmail.com或新浪微博:@张卫滨?1895联系到我。希望这本书对您有用!
张卫滨

前言: 在1995年的时候,设计模式曾经风靡一时。今天,我却发现情况完全相反。模式变得司空见惯,大多数的开发人员在日常工作中会不假思索地使用模式。现在,很少会出现像“四人组”(Gang of Four,GoF)模式2那样有影响力的新模式。实际上,这个产业从模式运动以来已经有了很大的改进。模式变得不再那么时尚,它们成了开发人员工具箱中的一部分并用来帮助设计软件系统。
但是,设计模式在过去十多年所扮演的角色不应被低估。它作为催化剂,推动面向对象开发成为主流。它们帮助大批开发人员理解了继承的真正价值以及如何有效使用它。模式提供了如何构建灵活且有弹性软件系统的深刻见解。借助金子般的智慧,如“优先使用组合而不是类继承”以及“面向接口编程而不是面向实现”(Gamma 1995),模式帮助一代软件开发人员接受了一种新的编程范式。
今天,模式还在广泛使用,但对很多开发人员来说,它们是很自然的事情。开发人员不再争论使用策略模式(Strategy pattern)的好处,他们也不必再参考GoF的书来识别哪个模式最适合当前的需要。相反,好的开发人员能够本能地设计面向对象的软件系统。
很多模式是永恒的。它们与特定的平台、编程语言或者编程时代无关。做一些细微的修改并且对细节稍加关注,一个模式就能变成适合给定上下文的形式。很多事情是与上下文相关的,包括平台、语言以及要解决问题的复杂程度。随着更多地学习模式,我们提供了在特定语言下如何使用模式的样例,并将其称为习语(idiom)。
我认为本书中的模块化模式也是永恒的。它们与特定的平台或语言无关。不管你是使用Java还是.NET、OSGi3还是Jigsaw4,或者是想构建更加模块化的软件,本书中的模式都会帮助到你。随着时间的推移,我们将会看到一些习语出现,它们将描述如何将这些模式应用到支持模块化的平台上,并且会有工具帮助我们使用这些模式重构软件系统。我希望当有工具出现的时候,它们能够不断地进化来帮助开发模块化的软件。但最重要的是,我希望在你们的帮助下,这些模式能够进化并且变成有助于设计更好软件的模式语言——这些软件将会实现模块化的优点。时间将会告诉我们一切。
面向对象的设计
在过去的几年中,出现了一些面向对象的设计原则。很多设计原则体现在设计模式中。Bob大叔提出的SOLID设计原则(参见附录)是最突出的例子。深入分析GOF模式,会发现它们中的很多都符合这些原则。
这些形成共识的知识以及所带来的收益有助于指导面向对象开发,但是创建大型的软件系统依然是很困难的。这些大型的系统依旧难以维护、扩展和管理。现有的原则和面向对象开发模式不能帮助管理大型软件系统的复杂性,这是因为它们所解决的是不同的问题。它们有助于解决逻辑设计相关的问题但是无助于解决物理设计方面的挑战。
逻辑设计与物理设计
有些原则和模式可以帮助解决软件设计和架构所面临的问题,这些问题几乎都是关于逻辑设计的。5逻辑设计是关于语言结构的,如类、操作符、方法以及包。识别类的方法、类之间的关系以及系统中包的结构都是逻辑设计问题。
毫无意外,因为大多数的原则和模式强调的都是逻辑设计,所以开发人员将大多数时间都用来处理与逻辑设计相关的问题。在设计类及其方法的时候,你是在定义系统的逻辑设计。决定一个类是否为单例(singleton)是逻辑设计问题。确定一个操作是否为抽象的或者决定要继承自一个类还是要包含它,这些同样也是逻辑设计问题。开发人员生存在代码中,就会不断地处理逻辑设计的问题。
能够很好地使用面向对象设计原则和模式是很重要的。要适应大多数业务应用中所需要的复杂行为是很有挑战性的任务,如果不能创建灵活的类结构,将会对未来的增长和可扩展性带来负面的影响。但是逻辑设计并不是本书关注的焦点。有很多其他的图书和文章提供了必要的指导,它们能提供足够的智慧以保证创建良好的逻辑设计。逻辑设计只是软件设计和架构所面临挑战的一个方面。挑战的另一方面就是物理设计。如果你没有考虑系统的物理设计,那么不管你的逻辑设计多么漂亮,可能都不会带来预期的收益。换句话说,缺乏物理设计的逻辑设计并不会带来预期的影响。
物理设计表现为软件系统中的物理实体。确定如何将软件系统打包到部署单元中是物理设计问题。决定哪个类属于哪一个部署单元也是物理设计问题。同样,管理可部署实体之间的关系也是物理设计问题。如果我们不说物理设计比逻辑设计更重要的话,起码它们是同等重要的。
例如,定义接口可以使客户端与实现该接口的类解除耦合,这是一个逻辑设计问题。按照这种方式解耦显然能够让你在不影响客户端的情况下,创建这个接口的新实现。但是,要将接口和它的实现类放到物理实体中就是物理设计问题了。如果这个接口有多个不同的实现,并且每个实现类都有底层的依赖,那么如何放置接口和实现就会对系统的整体软件架构质量产生巨大的影响。将接口和实现放在同一个模块中将会引入不必要的部署依赖。如果其中的一个实现依赖复杂的底层结构,那么不管你选择使用哪一个实现,都需要在所有的部署环境中包含这个依赖结构。尽管逻辑设计的质量很高,但是物理实体之间的依赖将会阻碍可重用性、可维护性以及很多在设计时试图要获得的其他收益。
令人遗憾的是,尽管众多团队付出了很大一部分时间在逻辑设计上,但是很少有团队在物理设计上下工夫。物理设计是关于如何将软件系统拆分为模块系统的,物理设计是关于软件模块化的。
模块化
在开发和维护方面,大型的软件系统天生就比小型的系统更复杂。模块化会涉及将大型的系统拆分为单独的物理实体,最终会使系统更易于理解。通过理解模块中的行为以及模块间存在的依赖,我们可以更容易地识别和评估变化所带来的连带影响。
例如,要修改具有很少输入依赖的软件模块比那些具有众多输入依赖的模块更容易。同样,具有很少输出依赖的软件模块比那些具有众多输出依赖的模块更易于重用。当设计软件模块时,重用和可维护性是重要的考虑因素,而依赖在其中扮演了重要的角色。但是,依赖并不是唯一的因素。
在设计高质量的软件模块方面,模块内聚也扮演着重要的角色。具有很少行为的模块对于其他的模块并没有做太多有用的事情,因此所提供的价值很小。与此相反,如果模块所做的事情太多将会难以重用,因为它所提供的行为超过了其他模块的期望。当设计模块时,识别合适的粒度等级是很重要的。太细粒度的模块提供的价值很小,可能还需要其他的模块协作才能发挥作用。太粗粒度的模块则会难以重用。
本书中的原则为设计模块化的软件提供了指导。它们探讨了一些方式,这些方式可以让你尽可能地减少模块之间的依赖同时又能最大化模块的重用能力。如果没有面向对象设计的原则和模式,其中的很多原则是无法实现的。你将会发现,对模块化系统所做的物理设计决策经常会影响逻辑设计决策。
模块化单元:JAR文件
在Java平台中,物理设计是通过仔细设计JAR文件的关系和行为实现的,模块化的单元就是JAR文件。尽管这些原则可以应用到其他的单元,如包中,但是将其应用在设计JAR文件中会特别有价值(关于模块的定义,参见第1章)。
OSGi
OSGi服务平台(OSGi Service Platform)是Java中的动态模块化系统。在OSGi的术语中,模块称为bundle。OSGi提供了一个框架来管理bundle,bundle被打包成普通的Java JAR文件,里面包含了清单文件(manifest)。在清单文件中包含了重要的元数据信息,这些信息描述了bundle以及对OSGi框架的依赖(关于OSGi,参见第13章)。
贯穿本书,你会发现使用OSGi的例子。但是,要使用模块化模式,OSGi并不是先决条件。OSGi只是提供一个运行时环境,它使得在Java平台中实现模块化成为可能,同时它也会强制要求这种模块化。OSGi提供了如下功能。
* 模块化:使得在Java平台中实现模块化成为可能,同时也会强制要求模块化。
* 版本管理:支持相同软件模块的多个版本部署在同一个Java虚拟机(JVM)实例中。
* 热部署:允许在运行时系统中进行部署和更新,不必重新启动应用或JVM。
* 封装:允许模块对它们的使用者隐藏实现细节。
* 面向服务:鼓励在更细的粒度上,在同一个JVM中使用面向服务的设计原则。为了做到这一点,OSGi使用的是μService。
* 依赖管理:需要明确声明模块之间的依赖。
本书所面向的读者
本书面向负责开发软件应用的开发人员和架构师。如果你对提升系统的设计感兴趣,那么本书也适合你。
本书并不只针对那些使用原生模块化平台的人们。例如,如果你正在使用OSGi,那么本书会帮助你使用OSGi设计更为模块化的软件。但是如果没有使用OSGi,本书所讨论的技术依然是很有价值的,它会帮助你使用这些技术来提升软件系统的模块化水平。本书也不是仅仅面向Java开发人员的。尽管本书中的例子使用的都是Java,但是所讨论的技术却可以很容易地用于其他平台,如.NET。
如果你想更深入地理解模块化的好处并且要开始设计模块化的软件系统,本书就是为你而作!本书为以下问题提供了答案。
* 模块化所能带来的收益是什么以及它为何如此重要?
* 如何让其他开发人员相信模块化的重要性?
* 要增加软件系统的模块化程度,可以采用什么技术?
* 在没有OSGi这样的原生模块化平台上进行开发,如何实现模块化?
* 如何将大规模的整体应用迁移为具备模块化架构的应用?
本书是如何组织的
本书分为三部分。第一部分描述模块化的理由。在这里,你将会看到模块化在设计软件时所扮演的重要角色,与此同时还会学习需要设计模块化软件的原因。第二部分是18个模式的列表,这些模式会帮助你设计更为模块化的软件。第三部分引入OSGi,然后阐述如何使用本书中的模式来设计软件系统并充分发挥模块化平台的优势。第三部分使用代码样例论证我们的观点。
显然,我建议你逐页阅读本书。但是,在阅读的时候你可能会愿意从一章跳到另一章。尽可以这么做!在本书中,你可能会发现许多与当前主题有关的向前或向后的交叉引用。这会帮助你进行导航并且更容易理解某个理念。以下是每章的概述。
第一部分:模块化的理由
第一部分阐述了模块化为何如此重要。这是采用模块化的理由。第一部分各章的概述如下。
* 第1章:该章引入模块化,正式地定义和识别软件模块的特征。建议每个读者都阅读这个简短的章节。
* 第2章:模块化有两个方面,即运行时模型和开发模型。一直以来,很多的关注集中在运行时模块化支持方面。随着越来越多的平台提供运行时模块化的支持,开发模型的重要性将会得到更多的关注。开发模型由编程模型和设计范式组成。
* 第3章:模块化在软件架构方面扮演着重要的角色。它填补了一项空白,这项空白从团队开发企业级软件系统以来就一直存在。该章将会探讨软件架构的目标以及模块化在实现这一目标时所扮演的重要角色。
* 第4章:企业级软件系统充满复杂性。团队会面临技术债的挑战,因为设计腐化,系统会面临崩溃。该章展现模块化如何帮助我们征服软件系统不断增长的复杂性。
* 第5章:重用是软件开发的灵丹妙药。但是,很少有组织能够真正实现高度的重用。该章考察阻止实现重用的障碍并探讨模块化如何帮助我们提高成功的可能性。
* 第6章:模块化与SOA在很多方面都是互补的。该章探讨模块化与SOA如何成为强大的组合。
* 第7章:为要讨论的理念提供一些合适的样例是很重要的。这一章有两个目的。首先,它将前6章的材料组织在一起,这样就能够看到这些理念是如何得到运用的。其次,它为第二部分要讨论的很多模式奠定了基础。
第二部分:模式
这一部分是由模块化模式的集合组成的。它们分成五类,每一类的目的稍有不同。不同的类别之间又会有所权衡。例如,可用性模式的目的在于使模块更易于使用,而扩展性模式会使模块更易于重用。可用与重用之间的权衡在第5章中会进行进一步的讨论。
* 第8章:基本模式是其他很多模式赖以生存的基础。它们为设计模块化的架构奠定切实可行的思考过程。它们关注将模块作为可重用单元、依赖管理以及内聚。如果要实现设计良好的软件系统,这些都是很重要的。
* 第9章:我很惊讶地发现开发团队花了如此多的时间来设计类之间的关系,但是在创建支撑的物理结构方面几乎不会花什么时间。在这里,你会看到帮助你创建模块间低耦合结构的一些指导。你也会看到模块设计会如何影响部署的讨论。
* 第10章:尽管耦合是很重要的测量指标,但内聚也同样重要。如果将所有的类都扔到两三个JAR文件中,这很容易创建并且管理依赖也很简单。但如果这样做,会使维护很困难。在该章中,我们将会看到那些有助于确保模块是内聚单元的模式。你会发现在依赖模式和可用模式之间会有一些很有意思的权衡。我会讨论这种权衡,以及你可以做些什么来对其进行管理。
* 第11章:设计软件系统的一个目标就是能够在不修改已有代码库的前提下对系统进行扩展。在达成这个目标时,抽象扮演了核心的角色,但是往已有系统中添加新功能只是我们所面临的挑战的一部分。我们还希望能够在不重新部署整个应用的情况下,部署这些新增的功能。扩展性模式的关注点在于帮助我们达成这个目标。
* 第12章:通用模式有助于实现模块化开发。与其他的那些模式不同,它们不强调重用、可扩展性以及可用性。相反,通用模式会讨论强制实现模块化的方式并有助于解决与质量相关的问题。
第三部分:模块化架构模式与OSGi
要使用本书中的模式,标准Java已经为你提供了所需的所有内容。但毫无疑问,你会希望在良好支持模块化的环境中查看这些模式。在这一部分,我们会这样做并借助OSGi框架以样例的方式进行阐述。
* 第13章:该章对OSGi进行简要的介绍,包括其功能和收益。该章并不想成为一个教程,我们假设读者已经掌握了OSGi的基本知识。这里会讨论模块化与OSGi,包括μService与Blueprint规范。除此之外,你还会看到OSGi的动态性如何为运行时环境带来模块化。最后,我们会探讨模式如何与OSGi中的开发相关联。借助OSGi,我们可以更容易地以最纯粹的形式使用某些模块化模式。
* 第14章:在模式讨论的过程中,我们使用了一个通用的贷款样例系统。在该章中,我们将会再次使用这个贷款样例,但是会将应用进行重构使其能够运行在OSGi环境中。你会很惊讶地发现,一旦具备了模块化的架构,OSGi只有一步之遥。
* 第15章:Java平台支持多种语言,OSGi并不会阻碍你使用Java平台上的替代语言。该章将会展示如何创建Scala模块并将其插入系统中。你会发现这有两个主要的优势。首先,模块化的架构能够很容易地添加代码,而不用对系统中已有的代码做任何修改。其次,它清晰地阐述了OSGi的动态性。
* 第16章:就像第15章的Scala样例一样,我们使用Groovy编程语言开发另外一个模块来进一步阐述模块化运行时环境所具备的灵活性和动态性。
* 第17章:模块化和OSGi的未来是什么?它会如何改变我们对大型企业级软件系统的思考方式?在本章中,将会以非常超前的方式探讨模块化和OSGi的未来。
模式形式
每个模式都具备一致的结构,从而保证有尽可能强的可读性。每个模式同时还会有一个配套样例阐述如何应用其底层的理念,但并不是每个模式都具有所有的部分。在有些场景下,某些部分可能会被忽略,因为前面的讨论已经引用过了。模式的主要结构类似于“四人组”(GoF)的格式,也就在《设计模式:可复用面向对象软件的基础》一书中所用的格式,它们的结构如下:
模式名称
首先,呈现的是模式名称。名称很重要,因为它有助于在开发人员之间建立一个通用的词汇表。
模式表述
模式表述概要地描述该模式,这个表述有助于理解模式的意图。
图示
图示以图形化的形式展现模式的整体结构。通常,这里会使用统一建模语言(Unified Modeling Language,UML)。
描述
描述更详细地展现模式所解决的问题。描述会有助于理解模式背后的驱动力。
多种实现
不管何种模式,将其用于解决现实世界中的问题时,很快就会出现微妙的实现细节。当使用模式时,“多种实现”将会讨论你应该考虑的一些重要的替代方案。
效果
所有的设计决策都会有有利的方面和不利的方面,就像软件设计的大多数建议一样,使用模式也要进行明智的判断。尽管它们提供了大量的灵活性,但是这些灵活性也是有代价的。“效果”这一部分会讨论一些有趣的事情,也就是使用模式时可能会遇到的问题以及可能出现的效果,由此可以判断在何种情况下应该忽略这个模式。在阅读完它的效果之后,关于何时使用这个模式以及何时使用替代的方案,你应该会有更好的理解。归纳起来,这一部分呈现使用这个模式的优势和劣势、你要付出的代价以及所能实现的收益。
样例
当关注样例的时候,你可能会更容易理解模式。这一部分会通过样例来阐述如何使用模式。有时候,我们会使用一些代码,而有时候,可视化的图片能够清晰地传达信息。最重要的是,样例不会存在于真空中。当在现实世界中使用模式的时候,模式之间会协同使用以创建更灵活的定制方案。在这种情况下,有些样例会建立在其他模式所讨论的样例之上。样例所能达成的结果就是让你更深刻地理解在实际工作中如何有效地使用这个模式。
小结
这一部分提供关于该模式的一些结论。
模式目录
以下为模块化模式的列表。
* 基本模式(Base Pattern)
* 管理关系(Manage Relationship):设计模块关系。
* 模块重用(Module Reuse):强调模块级别的重用。
* 模块内聚(Cohesive Module):模块的行为应该只服务于一个目的。
* 依赖模式(Dependency Pattern)
* 非循环关系(Acyclic Relationship):模块关系必须是非循环的。
* 等级化模块(Levelize Module):模块关系应该是等级化的。
* 物理分层(Physical Layer):模块关系不应该违反概念上的分层。
* 容器独立(Container Independence):模块应该独立于运行时容器。
* 独立部署(Independent Deployment):模块应该是独立的可部署单元。
* 可用性模式(Usability Pattern)
* 发布接口(Published Interface):使模块的发布接口众所周知。
* 外部配置(External Configuration):模块应该可以在外部进行配置。
* 默认实现(Default Implementation):为模块提供一个默认实现。
* 模块门面(Module Facade):为具有底层实现的细粒度模块创建一个门面,使其成为细粒度模块的一个粗粒度入口。
* 扩展性模式(Extensibility Pattern)
* 抽象化模块(Abstract Module):依赖于模块的抽象元素。
* 实现工厂(Implementation Factory):使用工厂来创建模块的实现类。
* 分离抽象(Separate Abstraction):将抽象与实现它们的类放在各自独立的模块中。
* 通用模式(Utility Pattern)
* 就近异常(Colocate Exception):异常应该接近抛出它们的类或接口。
* 等级化构建(Levelize Build:):按照模块的等级执行构建。
* 测试模块(Test Module):每个模块应该有一个对应的测试模块。
代码
贯穿整本书有大量的样例,在很多样例中包含代码。本书中所有模式的样例可以在以下的GitHub仓库找到:https://github.com/pragkirk/poma。
如果你想在自己的机器上运行代码却并不熟悉Git,可以通过以下地址查看Git的文档:http://git-scm.com/documentation。
第7章的样例可以在Google Gode Subversion仓库找到,地址为:http://code.google. com/p/kcode/source/browse/#svn/trunk/billpayevolution/billpay。
建议每位读者都能从以上的仓库中下载代码,并在阅读每个模式的“样例”章节时,使用这些例子。尽管在很多模式中都提供了代码,但是模式的样例章节中不可能把所有的代码都包含进来。书中的这些代码会在讨论中给你指导,并且对于模式如何使用会给你一个总体的印象。但是,通过下载并学习代码,你能够对模式的复杂性有更清晰的了解。
模式化模式的开放式想法
当我写作本书的时候,围绕模块化模式有一些争论。有些人建议将它们称为原则更恰当,而有些人更倾向于定律(law)这个词。有些人甚至建议将其称为启发(heuristic)、指导(guideline)、习语(idiom)、秘诀(recipe)或规则(rule)。但是到最后,所有的审校者都表示喜欢这本书的内容和表述方式。所以最终,我依然坚持称其为模式。相对于关注称其为模式、原则、启发或者什么其他方式,我更愿意你关注每一个模式所讨论的主题。这其中的理念才是重要的。
参考文献
Gamma, Erich, et al. 1995. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley.
致谢
本书的灵感来源于多个方面,最近几年我得到了他人极大的帮助。但是,我要特别感谢七个人。在我过去二十年的工作中、过去十年研发这些模式以及过去两年完成本书的过程中,他们的理念都给予了我指导。他们是:
Robert C. Martin(Bob大叔):Bob在面向对象设计原则方面(即SOLID原则)的工作是本书所讨论的很多技术的基石。实际上,这本书是他的这个系列的一部分,附录A提供了一些原则的整体介绍。
Clemens Szyperski:Clemens的《Component Software: Beyond Object Oriented Programming》6一书作为基础定义了模块的概念,这个概念贯穿本书。
John Lakos:John的《大规模C++程序设计》是我见过的唯一一本讨论物理设计的书。在那本书中,John的理念激发了我的灵感和对物理设计的兴趣,这使得我在过去的十年中使用并提炼技术,最终形成了这些模块化模式。
Ralph Johnson、John Vlissides、Erich Gamma以及Richard Helm(即GoF):除了提供本书中所使用的模式模板外,《设计模式》一书加深了我对面向对象理念的理解。
除此之外,我还要感谢下面这些人,得益于他们的反馈,我才能极大地改进本书所传递的信息。
尤其是,OSGi联盟的技术总监Peter Kriens:Peter提供了很多的反馈,以至于我应该将其列为合著者。
我还要感谢Brad Appleton、Kevin Bodie、Alex Buckley、Robert Bogetti、Chris Chedgey、Michael Haupt、Richard Nicholson、Glyn Normington、Patrick Paulin、John Pantone以及Vineet Sinha,他们提供了深刻的评论和有价值的反馈,帮助我更清晰地确定本书的范围并在讨论中加入更多视角。当然,在这个过程之中,还有很多人对我的著作产生了影响。令人遗憾的是,我肯定落下了其中一些人的名字。你们知道我说的是谁。谢谢你们!
当然,我还要感谢Prentice Hall团队,正是他们使这一切成为可能。编辑Chris Guzikowski多年以来给我了很多的机会来完成本书,这甚至超出了我的预期。策划编辑Sheri Cain给了我格式化方面的很多建议,回答了我所提出的一些很愚蠢的问题,并且将很粗糙的手稿进行了结构化和改善。编辑助理Olivia Basegio和Raina Chrobak在整个过程中都给予了我指导。项目编辑Anna Popick掌控了整个项目的完成。文字编辑Kim Wimpsett帮助出版了最终的手稿。
最后,我要感谢我的家庭。如果没有他们的爱,几乎什么事情都做不了,什么事情都没有意义。感谢父亲和母亲,他们的温柔陪伴着我的整个生命旅程。我相信很多时候他们都不明白我要成为什么样子。我的祖母Maude,您是有史以来最伟大的老师。我的孩子,Cory、Cody、Izi以及Chloe,他们使得生活充满了乐趣。当然还有我的妻子Tammy。她是我最好的朋友,正是她的鼓励让我抹去旧书稿的灰尘并重新开始。谢谢你们。感谢所有的人!

序言: 序1
我开心得手舞足蹈!我在墙上跳舞,我在天花板上跳舞!我欣喜若狂,我喜出望外。我真的非常开心。
你可能会问:“为什么呢?”我来告诉你为什么——因为终于有人读John Lakos的书了!
在20世纪90年代,John Lakos写过一本名为《Large-Scale C++ Software Design》1的书。这本书很棒,它是一本开创性的书。这本书为大规模应用架构制订了很好的方案。
John的书只有一个问题,那就是在书名中有“C++”这样的字眼,并且在出版的时候软件社区正在转向Java,所以真正需要读那本书的人并没有读到它。
但是当时做Java开发的人并没有阅读任何关于软件设计的书籍,因为这些人可能只有22岁,整日坐在舒适的办公室里,使用着Java,按日交易并梦想在23岁的时候成为亿万富翁。他们是如此抢手!
所以,十多年之后的现在就是这个样子了。我们成熟了一些,但也有很多失败。这些失败丰富了我们的经验。现在,需要回过头来看看自己所创建的Java架构废墟。我们怎么能如此幼稚?我们怎么能忽视Jacobson、Booch、Rumbaugh、Fowler以及Lakos所提出的原则呢?我们到底哪里做错了?
我要告诉你我们哪里做错了。Web迷惑了我们,我们的思想过多地受到了Twitter的影响。我们认为Web是革命性的。我们认为Web改变了所有的事情。我们认为Web让所有旧的规则过时。我们认为Web是如此之新、如此具有革命性、如此具有颠覆性以至于忽略了既有的游戏规则。
为此我们付出了代价。我们已经为此付出了高额的代价。我们为巨大的、无法管理的设计付出了代价。我们为复杂的、混乱的代码付出了代价。我们为误导性的、没有指导性的架构付出了代价。我们为失败的项目、破产的公司以及破碎的梦想付出了代价。我们在付出、不停地付出。
经过了15年,我们终于开始意识到原因了。我们开始看到游戏规则一点都没变。我们开始意识到Web只是另一种交付机制,与其他的并没有什么区别——它只是IBM老式绿屏请求/响应技术的一种变体。它就是原来简单的老式软件,我们永远不应该放弃这些游戏规则。
现在,我们意识到应该一直沿用Parnas、Weinberg、Page-Jones以及DeMarco这些人的智慧结晶。我们永远不能远离Jacobson和Booch的教导。我们早就应该读Lakos那本书!
不过,有人确实读过这本书。他肯定还读过一些其他的书,因为他专门写了一本书来描述Java架构的游戏规则,这本书比我以前见过的书都要好。现在你正把这本书拿在手中。写这本书的人名叫Kirk Knoernschild。
在这本书中,Kirk超越了Lakos、Jacobson以及Booch。他借鉴了这些大师的原则并创建了关于原则、规则、指导以及模式的全新组合,这些组合是很伟大的。朋友们,这是关于如何构建Java应用的。
继续翻阅一下。意识到什么了吗?是的,没有废话!都是核心内容。所有章节都是切题的,都是务实、有用且必要的!都是Java应用架构的具体要素——模块化的、解耦的、分层的、可独立部署的以及敏捷的。
如果你是Java程序员,如果你是技术领导或团队领导,如果你是架构师,如果你想要或必须让你的开发团队有所改变,那阅读这本书吧。如果你想避免重复过去15年的悲剧,那阅读这本书吧。如果你想学习软件架构到底是什么,那阅读这本书吧。
闲言少叙。
——Uncle Bob
序2
大约在两年前(2010年1月),我收到了Kirk Knoernschild的电子邮件,他邀请我为他即将完成的书提供反馈。回头看看随后发生的激烈讨论——大约有50封或更多的邮件——我不得不怀疑他对我会产生一些怨恨。我确信我们之间的交流导致他最初的进度出现了严重的延迟。所以,当Kirk邀请我为本书作序的时候,我感到又惊又喜;他为这本书付出了巨大的努力却让一个对手来给他写序,这说明他拥有足够强大的内心。
现在,我同意Kirk在本书中所说的大部分内容。我们都为模块化的魔力着迷,并且在大多数基础理念上都很有共识。但是,就像通常我们所见到的那样,最热烈的争论往往发生在那些在原则上互相认可但在细节上存在分歧的人群之间。直到在德国达姆施塔特举行的OSGi社区大会(OSGi Community Event)上(也就是这篇序言截止日期的两天前),我才突然理解了Kirk受到的阻力。
在这次大会上,Graham Charters(IBM)提出了“模块成熟度模型”(Modularity Maturity Model),这个模型来源于IBM的SOA成熟度模型(SOA Maturity Model),而后者又显然来源于最初的SEI能力成熟度模型(Capability Maturity Model,CMM)。这是一场很有见地的演讲,它使我认识到在系统设计方面,因为我在模块化的环境中生活了超过13年,我的观点并没有深受其影响。
CMM的一个重要教训就是不可能直接越过某一步。如果你的公司在CMM的第1级(混乱的),那么制订计划大跨步直接迈到第4级(一般称其为“已管理级”)不会是什么好主意。有的公司这样尝试了,但都惨烈失败了。每个中间步骤的转换都是必要的,它会帮助组织了解不同级别的复杂性。每个级别都有一系列自己的问题,这些问题需要在下一级别来解决。
在Graham演讲之后,我清楚地认识到我基本上都是从第5级往下看,而Kirk则是帮助人们从第1级往上看。我们之间争论的具体问题是设计模块化软件时所面临的挑战。当你到达第2级或第3级时,这些挑战是显而易见的,但当你处于第1级时,它们就不那么明显了。我们的大脑有这样一种定式,即只有我们处理过对应的问题,才能理解其解决方案。我试图让Kirk讨论的那些解决方案是他的读者还没有经历和理解的,这些问题位于前面的等级中。
在我的模块成熟度模型中(Graham的会稍有不同)有如下等级:
1)未管理的/混乱的
2)管理依赖
3)适当隔离
4)修改代码库(code base)以最小化耦合
5)面向服务的架构
在第1级中,应用是基于类路径(class path)的,也就是线性的JAR列表。应用包含一系列的JAR或具有类文件的目录,这些形成了类路径。在这个级别,根本不存在什么模块化。这个级别的问题是缺少类或版本混乱。
在第2级中,你已经识别了模块并声明了对其他模块的依赖。模块会有一个名字并且可以进行版本标识。它们依旧是线性查找的,第1级的很多问题依然存在,但是系统的可维护性以及结果的可重复性会更好一些。这个层级的主要问题在于,因为过度的传递性依赖,会过多地“从互联网上下载”。这是Maven目前所处的级别。
在第3级中,模块间会通过导入、导出和私有包真正实现模块间的隔离。依赖现在可以在包级别进行描述,这可以减少“从互联网上下载”。这种隔离为模块提供了内部的命名空间,而这个命名空间是真正在模块内部的,允许存在多个命名空间,这样在一个应用中就可以支持同一个包的不同版本。这个级别的问题通常是由流行Java模式引起的,因为它们基于动态类加载机制,很少能与模块边界和多命名空间兼容。
在第4级中,你的代码库的修改只是为了最大化内聚或最小化耦合。人们越来越认识到某一行代码可能会导致过多数量的依赖。组合或分隔系统的功能可能会对系统的部署产生重要的影响。在这个级别,已有的Java模式使用起来会很痛苦,因为它们通常会依赖中心化的配置,而这个方案更像是平等的点对点(peer-to-peer)模型。在OSGi中,μService变得很有吸引力,因为它们解决了很多问题。
在第5级,也就是最后一个级别中,模块化不再像它们所提供的μService那样重要。这时设计和依赖方案完全是通过μService实现的;模块只是使用和提供μService的容器。
在过去的13年中,我一直生活在第5级中,因为它是在OSGi中实现的。这有时会让我很难设身处地体会那些只使用类路径和简单JAR文件的人们。看到Graham Charter的演讲后,我认识到Kirk的目的是帮助人们理解模块化设计原则的重要性并使他们从第1级到达第2级,最终为他们打下坚实的基础以便使用OSGi实现更好的模块化。我认识到我经常试图把这本书直接提升到第5级,因为前面所提的一些教训对设计模块化软件也是很重要的。那样一本书依然是很必要的,希望有一天我能自己来写作。
现在Kirk的书很重要,因为它提供了开始模块化思考的模式并且能够让你借助最流行的平台、框架和语言构建模块化的软件。是的,我相信本书中的很多问题会有更好的解决方案,但是我也认为有时候追求更好会适得其反。
假设你使用Spring、Guice或者其他流行的依赖注入框架来构建Java应用,但依然面临脆弱和僵硬的软件,维护这些软件会很困难并且代价高昂,那么这本书对你来说真的有用。代码的完全耦合会造成很难添加新的功能或者修改已有的代码库。这本书将教会你很多模块化的基础课程并向你展示模块化的魔力。
也就是说,我还希望你特别关注贯穿本书使用OSGi的例子。它首先出现在第3章结尾处,它向你展示了OSGi如何借助μService来帮助你实现适当的隔离并尽可能最小化耦合的。随着本书所提供的价值不断增加,我相信按照它的建议将会帮助你构建架构上更加完整的软件并引导你正确地将应用迁移到OSGi上。OSGi是迄今为止最成熟的模块化方案。
一直以来,Kirk不仅仅是值得尊敬的对手;他敦促我把自己的想法写出来,通过这种方式让我学到了很多,比近几年任何人教我的都要多。在过去的两年中,在和他讨论这本书的过程中,我享受到了很大的乐趣,我真的希望你在读这本书的时候也会有同样的感觉。
——Peter Kriens
OSGi联盟技术总监

媒体评论: “基础永远不会过时。在本书中,Kirk介绍了如何立足基础,以低成本有效地构建高质量的软件密集型系统。你会发现这本书写得很好、很及时并且全是务实的理念。”
——Grady Booch,IBM院士
“与GoF的《设计模式》一样,Kirk的这本书应该成为每一位企业级开发人员和架构师的必备品,对Paremus的每一位工程师来说,这本书是必备读物。”
——Richard Nicholson,OSGi联盟主席、Paremus CEO
“通过写这本书,Kirk为软件社区做出了重要的贡献:他分享了自己关于模块化的大量真知灼见,这些内容新手能理解、在计算机课堂上能讲授并且有经验的程序员也可参考。我希望本书能够有广泛的读者。”
——Glyn Normington,Eclipse Virgo项目的领导者
“我们的行业需要开始思考模块化这个词——因而需要这本书!”
——Chris Chedgey,Structure 101创始人兼CEO
“在本书中,Kirk为我们提供了在现实世界中进行模块化软件开发所需要的设计模式。尽管模块化确实有助于管理复杂性和创建更容易维护的软件,但是天下没有免费的午餐。如果你想获得模块化所提供的收益,那么购买本书吧。”
——Patrick Paulin,Modular Mind咨询师和培训师
“Krik巧妙地记录了使用OSGi和Eclipse运行时技术的最佳实践。为了更好地理解如何创建优秀的软件,每一位高级Java开发人员都需要阅读本书。”
——Mike Milinkovich,Eclipse基金会执行总监

书摘: 第一部分
模块化的理由
市面上从来不缺乏软件设计方面的图书。有很多的书会教导你面向对象设计的基本和高级理念。你还会发现几乎有同等数量的书来教导你面向服务架构的基本和高级概念。但是,这里缺失了一部分:模块化。将系统的最高层架构和内部代码结合起来的深刻讨论,你应该很少见到。
在本书第一部分中,我们首先在模块这个词上达成共识。然后马上会讨论模块化的两个方面:运行时模型和开发模型。从这里开始,将会考察模块化怎样帮助我们实现“自上而下的架构”,这是通过将重要的高层架构组件与更具体的实现结合在一起做到的。复杂性是我们要征服的怪兽,而重用是我们想得到的灵丹妙药,模块化在这两个方面都会给我们提供帮助,我们会讨论它在这个过程中所扮演的重要角色。最后,在进入模块化模式之前,会通过一个样例练习介绍模块化的收益。在整个过程中,我们为使用模块化找到了充分的理由。
第1章 模 块 定 义
首先,需要回答一个简单的问题:
在Java平台中,软件模块是什么?
1.1 定义模块
简单来说,模块就是“一个软件块”(chunk of software)。不过,这不能很简洁地区别模块与其他软件块,如类、包、组件、服务甚至应用。所以,我们需要关注这个定义:
软件模块是可部署的、可管理的、原生可重用的、可组合的、无状态的软件单元,它为用户提供了简洁的接口。
这是大而全的一个定义,第1章就贸然介绍它也很不好。但是,在一些解释后,你就能领会模块是什么了。图1.1展示了这个定义。接下来,将探讨模块的各个元素。
图1.1 定义模块
1.1.1 可部署
模块是一个部署单元。不像其他软件实体,如类和包,模块是一个独立的部署单元,它能够与其他软件模块共处。从这个意义上讲,模块所表达的意义更多是物理层面上的,它比那些无形的软件实体如类或包的粒度更粗。可部署软件单元的例子包括EAR、WAR以及JAR文件。
1.1.2 可管理
模块是一个可管理的单元。在运行时模块系统中,模块可以进行安装、卸载以及更新。在开发过程中,将系统拆分成模块有助于简化很多其他的复杂活动。这包括提高构建的效率、允许开发人员独立地开发自主的模块并按照模块的边界规划开发工作。符合这部分定义的软件实体样例包括EAR、WAR以及JAR文件。
1.1.3 可测试
模块是一个可测试的单元。如同类可以使用测试驱动开发进行独立测试一样,模块也可以进行独立测试(参见12.3节)。符合这部分定义的软件实体样例包括类、包以及JAR文件。
1.1.4 原生可重用
模块是进程内(intraprocess)可重用的单元。尽管面向服务的架构(Service-Oriented Architecture,SOA)原则可以用来设计软件模块,但模块并不像应用或服务那样,它不是分布式计算技术。相反,以模块化组织部署单元的方式使得它们可以跨应用重用(关于重用,参见第5章),但是模块总是原生调用的。也就是说,模块暴露的操作是通过直接调用方法触发的。
重用模块的方式与重用服务也是不同的。一般来讲,服务只须部署一次并被多个使用者调用。因为模块是在进程内使用的,所以模块会与那些想重用其功能的进程一起部署。符合这部分定义的软件实体样例包括类、包以及JAR文件。
1.1.5 可组合
模块是可组合的单元。模块可以由其他模块组成。一般这指的是粗粒度的模块是由细粒度的模块组成的。(关于粒度,参见5.2.1节。)
1.1.6 无状态
模块是无状态的。特定版本的模块只会存在一个实例。我们不会实例化软件模块,尽管我们需要实例化软件模块中的类,这些类会保持状态,但是模块本身不会这样做。符合这部分定义的软件实体样例包括WAR、EAR以及JAR文件。
1.2 软件模块的简洁定义
进一步学习之前,我们先给出软件模块更简洁的定义。基于每部分的定义,可以更清楚地说:
Java平台中最适合的模块化单元就是JAR文件。
1.3 结论
在Java平台中,为了区分软件模块与其他“软件块”的概念,对模块的定义有一个简洁的共识很重要。在本书接下来的全部内容中,当提到模块时,指的就是Java平台中的JAR文件,它具备本章讨论的所有属性。
第2章 模块化的两个方面
模块化包含两个方面:运行时模型和开发模型。现在,关注的重点是运行时模型,出现了一些支持运行时模块化的框架。但最终,随着运行时模型的采用,开发模型的重要性将会占据主导地位。在本书中,关注开发模型的模式称为设计范式(design paradigm)。
2.1 运行时模型
运行时模型关注如何在运行时对模块化的软件系统进行管理。在Java平台中,模块化系统的事实标准是OSGi,很多应用平台借助OSGi运行时的能力增强模块化。在一定程度上,这使企业认识到OSGi的长处,不过他们所了解的并不够。随着厂商将OSGi添加到他们的产品中,很多组织会认识到这能带来两个好处,也就是更快地应用启动时间和平台适应性。(关于运行时的益处,参见13.2节)
但直到现在,众多广泛使用的平台在内部封装了OSGi却选择对企业级开发人员隐藏这一点。所以,开发人员无法构建充分利用模块化运行环境的应用程序。但是,这正在发生着变化,平台开始暴露OSGi的长处并允许开发人员利用其强大的运行时能力。我们不再受制于类路径地狱(classpath hell),庞大的应用程序也不会再折磨我们。相反,模块会在运行时发现其他模块,隔离应用程序的人工壁垒也不复存在。
最终,随着对模块化的支持迁移到平台中,企业级开发人员将使用这些框架和技术开发更加模块化的软件系统。当这种情况出现时,开发模型就会变得很重要了。
2.2 开发模型
开发模型解决的问题是开发人员怎样使用框架构建软件应用。开发模型又可以进一步分为两类:编程模型(programming model)和设计范式。在帮助开发人员构建更加模块化的软件应用中,两者都是很重要的。
2.2.1 编程模型
要利用运行时模块系统就需要程序员能够与模块系统的应用程序编程接口(Application Programming Interface,API)交互。但是,依赖这些API将会导致重量级(heavyweight)的模块(关于重量级,参见5.2.2节),它们很难测试并且很难在运行时系统外执行。为了减少对模块系统API的依赖,框架和一些技术必须提供一定程度的抽象,这样代码就不会直接依赖框架的API了。这些框架和技术的示例包括OSGi Blueprint服务、声明式服务(Declarative Service)、Spring Dynamic Module以及iPojo。
借助这些框架和工具,开发人员可以使用运行时模块系统的能力而不用再担心编程模型。这些框架封装了对API的依赖,这样代码就不用直接与这些API交互了。通过这些框架实现关注点分离能够让Java类依旧是简单旧式Java对象(Plain Old Java Object,POJO),它们不会依赖模块系统的框架。这使得编码和测试简单了许多。
2.2.2 设计范式
除了运行时模型和编程模型以外,在开发模块化软件时还会有其他的挑战。设计范式也是必须要解决的。一个组织如何创建更加模块化的架构?模块的合适粒度是什么样的?模块间应该存在什么程度的依赖?怎样最小化模块依赖?如何将大的模块拆分为一些更小的更加内聚的模块?怎样将已有的庞大软件系统模块化?应该在何时这样做?这些问题和其他问题是重要的架构和设计问题,这都是在企业级软件开发中与模块化相关的问题。本书中的模式就着重于设计范式。
为了理解设计范式的重要性,借助一些其他技术的历史,我们可以学到重要的经验教训。先看两个不同的技术示例,事实已证明它们与模块设计范式有一些相关性:面向对象(Object-Oriented,OO)编程以及企业级JavaBean(Enterprise JavaBean,EJB)。
1.面向对象
在20世纪90年代初期,面向对象范式被视为救世主。开发团队能够通过组合可重用的对象构建系统。面向对象范式承诺会显著缩减软件的上市时间并提供更高质量的软件。但承诺并没有兑现,面向对象范式的益处从来没有完全实现。有几个原因使得开发团队无法实现这些收益。作为可重用的基础,类的粒度太小。开发团队很难正确地把握和使用面向对象的理念。较深层次结构的继承以及包含太多功能的基类会导致设计不佳和脆弱的软件系统。总之,面向对象开发过早地失效了。
面向对象编程语言的运行时功能提供了如多态、动态绑定等特性,开发人员可以很容易地理解编程模型中的很多方面。使用点号调用方法以及定义私有的成员变量都是很简单的理念。但我们花费了很长的时间才理解怎样使用面向对象技术设计好的程序。换句话说,我们一直与设计范式做斗争。如今被视为面向对象设计的简单事实(“优先使用对象组合而不是对象继承”以及“面向接口编程而不是实现”) 在十五年前还是未知的,至少对我们并不是那样明显。即便是现在,我们还在学习新的方法来使用面向对象技术设计更好的软件系统。
2.企业级JavaBean
企业级JavaBean,尤其是其中的实体bean,曾被视为将Java开发的业务应用进行组件化的一种方式。EJB的运行时能力是很有吸引力的——事务、持久化、安全性、透明等——并且它们直接合并到了平台中。不过,有两个很明显的问题:开发模型复杂并且无法很好地理解。
尽管这已经是多年之前的事情了,但我清楚记得第一次接触EJB时的情形。我中途参加了一个正在进行开发的团队。那时,团队有超过100个的实体bean,本地开发环境需要四个多小时才能启动而且遍地都是问题。我坚持了三周,然后自愿离开了这个项目。在此之后不久,这个项目最终取消了。EJB的问题不在于运行时模型或编程模型。有很棒的工具使这些工作变得很容易。运行时模型实现了它的很多承诺,代码生成向导以及众多的工具使得编程模型相对很简单。甚至出现了一些框架使开发人员可以将代码与EJB编程模型解耦,这样他们就可以只设计更简单的POJO。但是,EJB是一项新的技术,很多开发人员缺少有效使用这项技术的设计能力。另外,事实证明,理解EJB相关的设计范式就是它的丧钟。
3.吸取的教训
面向对象的程序设计和EJB都曾经被视为有前途的技术,但事实证明,它们并没有达到最初所炒作的那样。问题不在于面向对象编程语言或实现EJB规范的平台,而在于我们怎样使用这些技术设计应用。最大的挑战是关于设计范式的。
这些教训为OSGi可能会面临的艰巨挑战提供了前车之鉴,具体来讲就是Java平台的模块化。如果开发人员不能很好地理解设计范式以及指导使用这项技术的原则和模式,那么运行时模型的优势将不能实现。要设计在架构方面灵活和适应性强的软件系统,模块化是一个关键因素。在Java平台中,确实存在模块化的需求,尤其是在开发大型企业级软件系统时。但是,如果现在不立即着手理解怎样设计更加模块化的应用,那么当支持模块化的平台出现时,我们将会面临重大的挑战。
2.3 模块化现状
乍看起来,运行时模型与开发模型有着密不可分的关系。运行时模块系统会使得设计模块化的软件更加简单,但对于设计模块化软件,它并不是必需的。实际上,本书第一部分的其余章节会讨论模块化所带来的明显益处,在了解完这些之后,开发团队最好可以立即模块化他们的应用,即便它们目前可能不会部署在支持运行时模块系统的平台上。如果你正在使用支持模块化的平台,那这对你会很有好处。但如果不是这样,那么你可能会想知道如何设计模块化的软件。
关于OSGi运行时模型,我们了解了很多,Jigsaw的细节也逐渐为人所知。 我们正在使用的很多平台在内部使用了OSGi运行时模型,即便它们现在并没有向我们暴露这一点。我们已经知道模块化单元是JAR文件,通过强调将JAR文件作为模块化单元并使用本书中的模式,现在就可以模块化我们的应用了。在第7章中,我们会直接看到在没有运行时模块系统的情况下,如何将一个系统进行模块化,这是通过将JAR文件作为模块化单元实现的。本书中很多模式的样例有意避免使用OSGi,不过我们会讨论当使用OSGi时这些模式该如何实现,OSGi会帮助我们进一步解释它们。在很多场景下,我们会包含使用OSGi的示例来阐述它所带来的优势。
当在应用中使用OSGi时,还需要记住的一点就是,对应用进行模块化的目的不仅是开发利用OSGi的应用,真正的价值在于它所带来的模块化架构。
注意
有很重要的一点需要注意,在没有运行时模块支持的情况下,对软件系统进行模块化会很有挑战性。在没有运行时系统时,你所使用的工具就很重要了。可视化工具可以帮助你理解结构,构建工具可以强制你保证应用中的结构,而依赖管理工具可以帮助你管理依赖,这些工具是很必要的。在本书中的很多样例中,将使用JarAnalyzer管理模块间的依赖(关于模块依赖,参见4.3节)。
另外,本书中的模式会帮助你对系统进行模块化。例如,等级化构建模式(Levelize Build pattern,参见12.2节)会帮助你在编译时实现模块间的关联关系。借助良好的工具和严格的纪律,模块化架构就会实现。另外,模块化架构实现之后一旦有运行时支持,就可以马上使用它了。但是,如果没有运行时支持,有一些事情是无法完成的。运行时模块化系统通常会支持以下的功能。
封装(encapsulation) :在标准的Java中,包中任何公开的类只要在类路径下就能被类路径下的其他类访问。换句话说,没有办法隐藏实现细节。所有的事情都是全局性的,这阻碍了模块化设计。运行时模块化系统提供了隐藏实现细节的能力。例如,OSGi可以使用?Service为底层的实现细节提供接口。
动态部署(dynamic deployment):在标准的Java中,更新软件通常需要重新启动JVM。运行时模块系统支持热部署。
版本管理(versioning):在标准Java中,不能为一个类部署多个版本。模块化的系统允许部署多个版本。
依赖管理(dependency management):在标准的Java中,没有办法实现模块的依赖结构。像Maven这样的构建工具试图通过描述依赖关系的JAR文件仓库解决这个问题。运行时模块系统可以实现运行时的依赖管理。
2.4 结论
模块化有两个方面:运行时模型和开发模型。开发模型包括编程模型和设计范式。所有的方面都是很重要的,但是如果不能理解如何设计模块化的软件将会降低使用模块化运行时或框架所带来的益处。本书主要讨论的是如何解决设计范式的问题。
第3章 架构与模块化
模块化在软件架构中扮演着重要的角色。它填补了自从用Java开发企业软件系统以来就一直存在的空白。本章将会讨论这个空白领域并探讨模块化是如何作为重要的中间技术填补这个空白的。
3.1 定义架构
关于架构(architecture)这个词,有多种定义。但是在这些定义中存在一个共同的主题以及一些关键词。以下是诸多定义中的一个,它由Booch、Rumbaugh和Jacobson提出(1999):
架构就是一系列重要的决策,这些决策涉及软件系统的组织、组成系统的结构化元素及其接口的选择、元素之间协作时特定的行为、结构化元素和行为元素形成更大子系统的组合方式以及引导这一组织——也就是这些元素及其接口、它们之间的协作以及组合——的架构风格。
现在看一下ANSI/IEEE Std 1471—2000(开放组织,the open group)的定义:
一个系统的基本组织,通过组件、组件之间和组件与环境之间的关系以及管理其设计和演变的原则具体体现。
在开放组织架构框架(The Open Group Architecture Framework,TOGAF)中,架构根据上下文有两个含义:
1)系统的正式描述,或者系统在组件级别的详细计划,这个计划会指导系统的实现。
2)组件的结构、它们之间的关系以及管理其设计和随时间演变的原则和指导。
查看这些定义会发现有很多共同的关键词,在这些定义中我们用黑体突出显示。重要的潜在含义都是由这些关键词呈现的。但是,这些关键词引出了一些重要的问题,为了完全理解架构就必须要回答这些问题。是什么使得一个决策具备架构上的重要性?组成元素是什么?我们如何适应架构的演化?为了实现模块化必须要做什么?在分析这些问题前,我想从一个关于软件架构的故事开始。
3.2 关于软件架构的一个故事
软件架构使我想到了下面的这个故事(霍金,1998):
一位著名的科学家(据说是贝特郎?罗素)曾经作过一次关于天文学方面的讲演。他描述了地球如何绕着太阳运动,以及太阳又是如何绕着我们称为星系的巨大的恒星群的中心转动。演讲结束之时,一位坐在房间后排的矮个老妇人站起来说:“你说的这些都是废话。这个世界实际上是驮在大乌龟背上的一块平板。”这位科学家很有教养地微笑着答道:“那么这只乌龟站在什么上面呢?”“你很聪明,年轻人,的确很聪明,”老妇人说,“不过,这是一只驮着一只一直驮下去的乌龟群啊!”
——《时间简史》史蒂芬?霍金
软件架构就是“一只驮着一只一直驮下去的乌龟群”。 这是怎样做到的呢?这一节将会讨论这些想法。
3.2.1 象牙塔
我们中的很多人可能都接触过象牙塔。在一些功能失调的组织中,架构师和开发人员不能有效地交流。结果就是双方都缺少透明度和理解。如图3.1所示,架构师将他们的智慧转送给开发人员,而开发人员不能将高层次的理念转化为具体的实现。经常会出现故障,这是因为(尽管我知道还有其他的原因)架构师关注广度而开发人员关注深度。每个组对软件架构都有不同的见解,尽管两者都信誓旦旦,但是在他们所理解的中间区域存在一个空白。架构师可能关注应用和服务,而开发人员可能更关注代码。遗憾的是,在他们的关注点之间还有很多事情。广度和深度之间的空白区域形成了象牙塔架构。
改编自http://www.rendell.org/jam/upload/2009/1/tower-12054835.jpg
图3.1 象牙塔(the Open Group)
3.2.2 乌龟和塔
毫无疑问,象牙塔架构的功能是有问题的,它的表现就是缺乏架构上的完整性。假设为了帮助架构师和开发人员,要怎样弥补广度和深度之间的空白呢?如何才能更有效地交流呢?怎样才能增加相互理解和透明度呢?
让我们通过另一种定义形式重新看一下软件架构的定义。我最欣赏的软件架构定义是Ralph Johnson提出的,Martin Fowler在一篇文章中(2003)对此进行了引用。他说:
在大多数成功的软件项目中,从事该项目的专家开发人员对系统的设计存在共识。这种共识称为 “架构”。共识包括如何将系统分为组件以及组件如何通过接口进行交互。这些组件通常会由更小的组件组成,但是架构只包括组件以及能够被所有开发人员所理解的接口……架构是重要的事情,无论它是什么。
这个定义与本章前面的那些定义的关键区别在于“共识”,它表示软件架构中有社会性的方面。对于系统如何拆分为组件以及它们之间怎样交互,我们必须建立共识。架构不仅是一些技术理念,它也是一个社会性的结构。通过架构的社会性方面,我们可以弥合架构师和开发人员之间的分歧。
为了保证共识,必须要实现自上而下的架构(architecture all the way down,参见6.1节)。架构师不能仅关注服务,开发人员也不能仅关注代码。每个组必须都要关注巨大的中间地带,如图3.2所示。
改编自http://www.rendell.org/jam/upload/2009/1/tower-12054835.jpg
图3.2 自上而下的架构
仅关注高层抽象是不够的。只强调代码质量也是不够的。我们必须通过其他方式消除这个断层,这包括模块和包设计。通常,在各种会议上演讲时,我会要求那些从事服务设计的听众举手,很多手举了起来。我还会要求那些从事类设计和代码质量的人举手,同样,有很多手举了起来。但是,当我要求那些从事包和模块设计的人举手时,只有很少比例的人举手了。
这是很令人遗憾的,模块和包的设计在重要性方面是与服务和类的设计相同的。但是在这个过程中,我们强调了服务和代码质量,而忽略了它们之间还有什么。在每个应用和服务内部都会出现腐化的设计,即便是在最灵活的代码上构建的应用和服务,也有可能充斥着重复难以理解的代码。富有弹性的包结构以及对应的软件模块将会帮助你消除服务和代码之间的断层。模块化是很重要的中间技术,它能够帮助我们实现自上而下的架构,这种架构会填充广度和深度之间的鸿沟。
我们需要关注模块化,以保证所讲述的架构故事始终是一致的。它是将一切绑定在一起的黏合剂。它有助于连接低层次的类设计与高层次的服务设计。它能帮助降低象牙塔、增强通信、增加透明性、确保互相理解并检查多个层级的一致性。它使得我们能够实现自上而下的架构并实现架构的目标。
3.3 架构的目标
模块化不仅有助于解决软件架构中社会性方面的问题,还会帮助我们设计更加灵活的软件系统,也就是,具备弹性、高适应性以及可维护性架构的系统。查看一下之前关于架构的定义,就会得到架构的目标。Fowler所引用的由Johnson给出的架构定义表明架构就是关于重要的事情的。在以下表述中,Booch明确说明如果有的事情很难改变,那么在架构上它就是重要的:
所有的架构都是设计,但并不是所有的设计都是架构。架构表现为重要的设计决策,这些决策会形成系统,在这里重要性是通过变化的成本衡量的。
基于以上表述,可以得到这样的结论:软件架构的目标必须是减少变化的影响和成本,从而消除架构上的重要性。通过创建灵活易变化的方案,我们试图使得一些事情在架构上不那么重要,如图3.3所示。但是这里存在一个悖论。
图3.3 架构的目标
3.3.1 悖论
消除架构并不是新的理念。实际上,Fowler在他的文章“谁需要架构师?”(Who Needs an Architect?,2003)中提到了“丢掉软件架构”(getting rid of software architecture)。通过减少变化的影响和成本来消除架构的必由之路就是灵活性。系统越灵活,就越能按需进行适应和进化。但是这里有一个悖论,Ralph Johnson的表述支持了这个观点(Fowler 2003):
……让每件事都易于变化会使得整个系统非常复杂……
随着灵活性的增长,复杂度(参见4.1节)也会随之增加。复杂性是我们要试图驯服的怪兽,因为复杂的事情比简单的事情更难以处理。可以肯定的是,没有明确的道路通往胜利。但是,如果既能驯服复杂性又能增加灵活性,如图3.4所示,那又会怎么样呢?现在探讨一下设计灵活的软件系统同时又不增加复杂度的可能性。可能吗?换句话说,怎样消除架构?
图3.4 最大化灵活性,管理复杂度
3.3.2 消除架构
正如对Johnson的引用中所明确指出的,设计无限灵活的系统是不可行的。因此,我们很有必要识别出在什么地方保证灵活性从而减少变化的影响和成本。但挑战在于,我们并不是总能在项目的初期就知道最终哪里会发生变化,所以对于我们不知道的事情无法创建一个灵活的方案。这就是预先设计的大架构(Big Architecture Up Front,BAUF)所面临的问题,这也是我们需要根据当时的情况做出架构决策的原因。换句话说,在有充足的知识保证我们能够做出最明智的决策之前,我们应当尽可能地延迟做出具体的架构决策,因为那样会使我们局限在一个具体的解决方案中。
当面临不确定时,对问题进行隔离并做出决策前我们要十分慎重并要确保当未知问题的答案出现时,初始决策能够灵活变化,我们这样做也是基于以上的原因。为此,模块化是被忽略的一个部分,它能够帮助我们尽可能地减少变化带来的影响和成本,这是我们使用模块化架构设计软件系统的驱动力。在《UML User Guide》 中,Booch提到了“为系统中的接缝(seam)建模”。他说(1999):
识别系统中的接缝涉及识别在系统体系架构中明确的分界线。在这些分界线的每一侧,都会发现可独立变化的组件,只要在分界线两侧的组件遵循由接口描述的契约,在一侧变化的组件就不会影响另一侧的组件。
Booch称为组件的地方,我们称为模块。Booch称为接缝的地方,我们称为结合点(joint,参见4.5节)。模块化以及设计模式和SOLID原则(参见附录),代表了我们尽可能减少变化的影响和成本的最好愿望,同时它也会消除变化所带来的架构重要性。
3.4 模块化:被忽视的部分
架构定义的两个关键因素是组件和组合(参见1.1节)。但是对于组件 (实际上,这使我想起了架构也是如此)这个词还没有一个标准的和公认的定义。大多数的地方都使用这个词代表“一块代码”(a chunk of code)。但那是不合适的,在OSGi环境中,模块显然是一个软件组件。开发具备适应性、灵活性以及可维护性架构的系统需要模块化,这是因为我们必须设计灵活的系统,以便在开发过程中面临变化时,能够根据当时的情况做出决定。模块化一直是被忽略的一个部分,它能够让我们更容易地适应变化并且关注系统中那些需要灵活性的特定领域,如图3.5所示。改变封装在一个模块中的设计要比改变分散在多个模块中的设计更容易一些。
它是真正的封装吗
在标准的Java中,没有办法强制将设计细节封装在模块中,因为Java没有提供将包或类定义为模块作用域的方法。于是,一个模块中的类总是能够访问另一个模块的实现细节。这就是像OSGi这样的模块化框架,能够发挥作用的地方了,因为它能够在清单文件头中显式声明导入包和导出包,进而强制将实现细节封装在模块中。即便是包中公开的类,只要这个包没有显式声明为导出,其他的模块也不能进行访问。区别很细小,但是很重要。在贯穿本书的模式中,我们将会看到几个这样的例子,当这些例子出现时,我会明确指出。现在,先看一个简单的例子(关于在没有运行时模块系统环境下的模块化,参见2.3节)。
图3.5 封装性设计
1.标准Java:没有封装
图3.6展现了Client类要依赖Inter接口,Impl提供了实现。Client类打包在client.jar模块中,Inter和Impl打包在provider.jar模块中。这是一个关于模块化系统的好例子,但它展现了在标准Java中,我们是无法封装实现细节的,因为没有办法阻止对Impl的访问。provider.jar模块外的类依然可以接触到Impl类,并且能够直接实例化和使用它。
实际上,Impl是作为包作用域的类进行定义的,如程序清单3.1所示。但是部署在client.jar模块中的Spring XML配置文件AppContext.xml依然可以在运行时创建Impl实例并将其注入Client中。AppContext.xml和Client类分别如程序清单3.2和程序清单3.3所示。关键问题在于AppContext.xml部署在client.jar模块中,而它创建的Impl类部署在provider.jar模块中。如程序清单3.2所示,部署在client.jar模块中的AppContext.xml违反了封装性,因为它引用了provider.jar模块的实现细节。因为Spring的配置是全局配置,所以结果就是违反了封装性。
图3.6 标准Java不能封装模块中的设计细节
程序清单3.1 类Impl
程序清单3.2 Spring配置文件AppContext.xml
程序清单3.3 类Client
2.OSGi与封装
现在,来看看相同的例子用OSGi(参见第13章)如何实现。这里,provider.jar模块中的Impl类严格进行了封装,Impl对其他模块中的类是不可见的。Impl类和Inter接口与前面的例子中是一样的,不需要任何变化。我们将已存在的应用放在了OSGi框架中,它会强制封装模块的实现细节并提供模块间进行交流的一种机制。
图3.7展示了这种新的结构。实际上,它是抽象化模块模式(Abstract Modules pattern,参见11.1节)的一个样例。这里将Spring XML配置文件拆分成了4个不同的文件。本来我可以只使用两个配置文件,但是对于每个模块我希望将标准Java和OSGi框架的配置分离开。provider.jar模块负责对自己进行配置并在安装时暴露它的服务功能。在描述这种方式之前,这里对每个配置文件进行简单的介绍。
图3.7 使用OSGi实现的封装性设计
client.xml:标准的Spring配置文件,它描述OSGi该如何启动应用。
client-osgi.xml:Spring配置文件,它允许Client类使用OSGi ?Service。
provider.xml:Spring配置,包含provider.jar模块中的bean定义。
provider-osgi.xml:Spring配置,它将provider.xml中的bean定义为OSGi ?Service。
在了解这两个模块如何装配在一起之前,先看一下provider.jar模块,它包含了Inter接口、Impl实现以及两个配置文件。重复一遍,Inter和Impl与前面的样例是相同的,所以我们只看一下配置文件。图3.7中的provider.xml文件定义了标准的Spring bean配置,也就是之前AppContext.xml文件所展示的那样。程序清单3.4展现了provider.xml文件。关键的一点在于这个配置文件要部署在provider.jar模块中。不能试图在provider.jar模块外实例化Impl类。因为OSGi保证了封装,所以任何试图访问模块实现细节的行为将会导致运行时错误,如ClassNotFoundException异常。
程序清单3.4 provider.xml配置文件
OSGi如何阻止其他的类直接实例化Impl类呢?provider.jar模块中的清单文件(Manifest.mf)只提供了com.p2包中的类,并不含com.p2.impl包。所以,注册为OSGi ?Service的Inter接口能够被其他的模块访问,但是Impl类就不能被访问了。程序清单3.5展示了清单文件中导出包的那一部分。
程序清单3.5 provider.jar模块的Manifest.mf文件声明了所要导出的包
provider-osgi.xml文件使得事情变得很有意思,通过它将provider.jar模块的行为提供为OSGi ?Service,这个服务会作为Client和Impl之间的协议。为provider.jar模块提供配置的文件放在了provider.jar模块中,所以并没有违反封装的事情发生。
程序清单3.6展示了这个配置。使用OSGi框架注册的?Service服务称为interService,它引用了程序清单3.4中定义的Impl bean,在这里以Inter类型提供行为。此时,provider.jar模块有一个名为interService的OSGi ?Service,它可以被其他的模块使用。在provider.jar模块在OSGi框架中安装并激活后,这个服务就可用了。
程序清单3.6 provider-osgi.xml配置文件
现在,看一下client.jar模块。client.xml文件会配置Client类。它实际上将程序清单3.3中Client类的main方法替换为run方法,OSGi框架会实例化Client类,使用Inter类型对其进行配置,并会调用run方法。程序清单3.7和程序清单3.8分别展现了client.xml文件和Client类。这种机制会进行初始化处理并取代之前样例中Client类的main方法。
程序清单3.7 client.xml配置文件
程序清单3.8 Client类
注入Client类中的Inter类型是通过client-osgi.xml配置文件完成的。这里声明需要使用一个Inter类型的?Service,如程序清单3.9所示。
程序清单3.9 client-osgi.xml配置文件
client.jar模块的清单文件导入了com.p2包,这样就可以访问Inter ?Service了。程序清单3.10展示了client.jar模块导入和导出包这部分的清单文件配置。
程序清单3.10 client.jar模块的Manifest.mf文件
这个简单的例子有多个很有意思的设计。 provider.jar模块是独立部署的(Independent Deployment pattern,独立部署模式,参见9.5节)。它不依赖其他模块,并且将自己的行为提供为?Service。在这个系统中,没有其他模块需要知道这些细节。
将Impl类和Inter接口分别打包到不同模块中,会使设计更灵活。通过将接口从实现中分离,可以给系统带来更大的灵活性,尤其是在OSGi管理模块的情况下(Separate Abstraction pattern,分离抽象模式,参加11.3节)。
乍看起来,它似乎也违反了外部配置模式(External Configuration pattern,参见10.2节)。在为一个模块定义外部配置的时候,还希望能够保证将实现细节封装起来。外部配置更多的是允许客户端配置模块到上下文环境中,并不是关于提供模块实现细节的。
这个示例所能带给我们的最关键一点就是provider.jar模块中的类进行了严格的封装,因为OSGi框架强制实现了类型的可见性。我们只暴露了模块导出包中的公开类,?Service机制使得模块间能够以一种很灵活的方式交流。?Service跨越了系统的结合点(参见4.5节),因为OSGi是动态的,所以对?Service的依赖也是如此。?Service的实现可以在运行时添加或移除,系统可以在它们出现时绑定新的实例。
在后面的讨论中,会看到更多的例子。即使使用标准的Java无法强制封装模块的实现,但设计更加模块化的软件系统依然是很必要的。你将会看到,通过使用本书中讨论的一些技术,当使用运行时模块系统时,我们会占据一个很有利的位置。
3.5 回答我们的问题
之前,在引入软件架构的三种定义时,我们曾经提出过如下问题。通过前面的解释,我们回答了这些问题。但为了更清楚一些,让我们再简单回答一下:
是什么使一个决策具备架构上的重要性?如果变化的影响和成本很大,那么这样的决策就具备架构上的重要性。
组成元素是什么?组成元素包括类、模块以及服务。
怎样适应架构的演化?演化是通过设计灵活的解决方案实现的,这样的方案能够适应变化。但是灵活性也会带来复杂性,我们必须在系统合适的地方构建灵活性。
3.6 结论
架构的目标是尽可能减少变化的影响和成本。模块化通过填补高层架构组件以及底层代码之间的空白,帮助我们实现这个目标。模块化是帮助我们增加架构敏捷性的重要媒介。它填补了架构师和开发人员之间的断层。借助它,我们可以创建适应变化的软件架构。模块化能够帮助我们实现自上而下的架构。
3.7 参考文献
Booch, Grady, James Rumbaugh, and Ivar Jacobson. 1999. The Unified Modeling Language User Guide. Reading, MA: Addison-Wesley.
The Open Group.The Open Group Architecture Framework.www.open-group.org/architecture/ togaf8-doc/arch/chap01.html
Hawking, Stephen. 1998. A Brief History of Time. Bantam.
Fowler, Martin. 2003. “Who Needs an Architect?” IEEE Software.
Booch, Grady. 2006. On Design.
www.handbookofsoftwarearchitecture.com/index.jsp?page=Blog&part=All