设计模式与小粒度架构设计

p6dp

贡献于2014-04-26

字数:0 关键词: 软件架构

◆中科院计算所培训中心 高级软件系统架构师培训 设计模式与小粒度架构设计 在高层架构设计中,我们已经详尽的讨论了为达到系统的质量目标而采取的架构策略, 但是这一策略的实现,必须依靠小粒度架构的良好设计。 小粒度设计阶段考虑的主要是模块结构设计,它着力于解决如下问题: 1,给出软件结构中各模块的内部过程描述。 2,模块的内部过程描述也就是模块内部的算法设计。 3,模块设计有时需要导出一些算法设计表示,由此可以直接而简单地导出程序代码。 模块结构设计直接对应于实现编码,因此设计质量直接影响着软件的质量。为了合理的 规划模块之间的关系以及模块内部的各个类之间的关系,就需要提出一些原则,这就是设计 模式提出的原因和背景。设计模式也称之为微观架构模式,这种小尺度的对象和框架的设计, 有时候也需要资深程序人员参与。 7.1 软件重构技术 关于系统可维护、可集成性的实践,引发了人们对于这类问题理论上研究的兴趣,一个 直接的成果就是软件重构技术的提出。软件重构技术是对软件的内部结构所作的一种改变, 这种改变在可观察行为不变的条件下使软件更容易理解,而且修改更廉价。 7.1.1 为什么要研究重构技术 在软件开发与维护的长期实践中,人们普遍认识到的一个事实是代码太容易变坏。 坏的代码总是趋向于有更大的类、更长的方法、更多的开关语句和更深的条件嵌套。特 别是那些初看相似细看又不同的代码泛滥于整个系统:条件表达式,循环结构、集合枚举…。 全部代码都以非常密集的样式被书写,你根本看不到这种代码还有什么良好的设计。 当项目进行或者完成之后,需求被改变,功能被增加,程序员改变代码,所有这些情形 都倾向于在非常紧的工期下发生。这些因素意味着代码不被维护,只是被扩展,导致难于阅 读和理解的复杂代码。随着时间的漂移,代码中只有很少的一部分代表了系统的设计。 如果要在这样一种系统上添加功能,第一个反应应该是拒绝。因为这样的代码难以理解, 更不要说对它加以修改。存在的第二种可能性是抛弃现在的系统,然后从头编写。如果项目 的规模较大,或者系统已经在运行,不可能进行重新编写,这时人们就得面对第三种选择, 硬着头皮进行维护,进行修改和扩充。 维护人员通常采取一种快速和消极的方法。如果系统有问题,那么就尽快地找到问题, 直接修改它。如果要增加一项新功能,就会从原来的系统中找到一块相近的代码,拷备出来, 作些修改,再粘贴进去。这样一来,系统变得越来越难以理解,维护越来越困难、越来越昂 贵。系统变成了一个十足的大泥潭。每个人都不愿意看到这种情况,但奇怪的是,这样的情 形却一次又一次地在现实中不断地出现。 所有维修都倾向于破坏结构,增加了系统的凌乱。在修理原始设计缺陷上所花的时间越 来越少,越来越多的时间是花在修理由早期修理所引入的错误上。随着时间的流逝,系统变 得越来越混乱。迟早修理变得不可能而停止下来。 软件重构技术的动机是研究主要包括软件重用、软件维护、和软件的重新组织(Software Restructuring)。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 1,软件重用(Software Reuse) 为了降低开发软件的高成本,软件重用的研究是为了使一个系统开发的知识再次容易地 用于另一个软件系统的开发。然而,可重用软件经常需要很多设计迭代。使软件更容易改变 将使设计迭代更简单。这样的软件将更可重用。 抽象、封装、继承、多态和模块性这样的面向对象程序设计特性给软件再利用提供了 基础结构,以鼓励再利用现有的代码,而不是从头开始编码。这样,通过添加新类或者在现 存类上添加操作能对软件作出某些改变,而使软件的大部分保持不变。 除代码级再利用之外,设计级的再利用也是研究的内容。而且,从长期的观点来看, 人们认识到设计级的再利用更为重要。面向对象应用框架(framework)是这种研究努力的 结果。框架是抽象和具体类的集合。从而,能够添加新的子类对它进行重定义。因此,框架 支持抽象级,并允许部分的规范说明。 2,软件维护(Software Maintenance) 软件重用与软件维护紧密相关。维护是软件生产所有方面中最为困难的。主要的理由在 于维护包容了软件过程所有其他阶段的各个方面内容。在软件生命周期中,在维护上所花的 时间比任何其它阶段都更多。实际上,现行软件的维护工作量能占到全部开发工作量的 60 % 以上。软件维护经常需要重新组织软件。 3,软件重新组织(Software Restructuring) 不适当的设计方法学,缺乏开发和维护标准等诸如此类的很多因素都能导致拙劣的软件 结构。只存在一些不对代码进行更改的方法。例如,人们能够在软件再工程期间从代码和现 有的文档出发,重新创建软件的结构。 7.1.2 重构的定义 重构是以各种方式对一对象设计进行重新安排使之更灵活并且/或者可重用的过程。效 率和可维护性可能是进行重构最重要的理由。 重构定义为名词形式和动词形式两部分: 重构(Refactoring,名词):是对软件的内部结构所作的一种改变,这种改变在可观察 行为不变的条件下使软件更容易理解,而且修改更廉价。 重构(Refactor,动词):应用一系列不改变软件可观察行为的重构操作对软件进行重新 组织。 这些定义中最重要的方面是不改变软件系统的可观察行为,并且改变软件结构是朝着更 好的设计和更能理解,而且可重用的方向进行。 重构的名词形式就是说重构是对软件内部结构的改变,这种改变的前提是不能改变程序 的可观察的行为,这种改变的目的就是为了让它更容易理解,更容易被修改。 动词形式则突出重构是一种软件重构行为,这种重构的方法就是应用一系列的重构操 作。 7.1.3 重构的原则 1,一个时刻只做一件事情 如果你使用重构开发软件,你把开发时间分给两种不同的活动:增加功能和重构。 增加功能时,你不应该改变任何已经存在的代码,你只是在增加新功能。这个时候,你 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 增加新的测试,然后让这些新测试能够通过。当你换一顶帽子重构时,你要记住你不应该增 加任何新功能,你只是在重构代码。你不会增加新的测试。只有当重构改变了一个原先代码 的接口时才改变某些测试。 在一个软件的开发过程中,你可能频繁地交换这两件工作。 关于两件工作交换的故事不断地发生在日常开发中,但是不管你做哪件工作,一定要记 住一个时刻只做一件事情。 2,小步前进 保持代码的可观察行为不变称为重构的安全性。重构的另一个原则是小步前进,即每一 步总是做很少的工作,每做少量修改,就进行测试,保证重构的程序是安全的。如果你一次 做了太多的修改,那么就有可能介入很多的错误,代码将难以调试。 这些细小的步骤包括:确定需要重构的位置,编写并运行单元测试,找到合适的重构并 进行实施,运行单元测试,修改单元测试,运行所有的单元测试和功能测试等。如果按照小 步前进的方式去做重构,那么出错的机会可能就很小。 小步前进使得对每一步重构进行证明成为可能,最终通过组合这些证明,可以从更高层 次上来证明这些重构的安全性和正确性。 7.1.4 重构的目标和本质 从概念上讲重构的目标是对软件系统的设计进行重新组织,使系统满足某些通用设计的 准则,并具有容易扩展系统功能的结构或者形状。在重构应用与实践中,设计模式(design pattern)为重构提供了一个明确的目标。 重构的实质是在保持可观察行为不变的前提下,为提高软件的可理解性,可扩展性和可 重用性而对软件进行的修改。 7.1.5 重构的组成与步骤 重构由许多小的步骤组成。当一次对系统作了很多改变时,在此过程中也极有可能引入 许多错误。但产生这些错误的时间和地点是不可再现的。如果以小步前进的方式实现对系统 的改变,并在每一步后运行测试的话,错误就有可能在它引入系统后的测试中立即表现出来。 然后对每步的结果进行检查,如果有问题,可撤消此步所作的改变。在复原之后,可以采取 更小的步骤前进。 重构软件系统采取的步骤如下: 1.确定需要重构的位置。可以通过理解,扩展,或者重新组织系统来发现问题,或者 通过查看实际代码中的代码味道(code smell)来确定位置,或者通过某些代码分析工具。 2.如果所考虑的代码的单元测试存在的话,就运行该单元测试看看能否正确的完成。 否则,编写所有必要的单元测试并运行它们。 3.通过思考或者查看重构分类目录,找出能够明显被应用的重构操作。 4.遵循小步前进的原则实现重构操作。 5.在每步间运行测试以保证行为未被改变。 6.如有必要,对作过改变的接口修改测试代码。 7.当重构操作被成功地用于重新组织代码,再次运行测试,集成并运行全部的单元测 试和功能测试。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 7.1.6 重构的优点与风险 重构增加了具有某种缺点的程序的价值。难读的程序难修改。具有重复逻辑的程序难修 改。带有复杂条件逻辑的程序更难修改。重构虽然需要更多的“额外工作”,但是它给我们 带来的各种好处显然值得我们做出这样的努力。 1,简化测试 一个好的重构实现能够减少对新设计的测试量。因为重构的每一步都保持可观察的行为, 也就是保持系统的所有单元测试都能顺利通过。所以只有发生改变的代码需要测试。这种增 量测试使得所有的后续测试都建立在坚实的基础之上,整个系统测试的复杂性大大降低。 2,增进软件可理解性 程序编写是人的活动,人首先要理解才能行动。所以,源代码的另一个作用就是用于交 流的工具。其他人可能会在几个月之后修改代码,如果连理解代码都做不到,又如何完成所 需的修改呢? 我们通常会忘掉源代码的这种用处,尽管它可能是源代码更重要的用处。不然,我们为 什么发展高级语言、面向对象语言,为什么我们不直接使用汇编语言甚至是机器语言来编写 程序? 如果一个人够理解我们的代码,他可能只需要一天的时间完成一个增加功能的任务,而 如果他不理解我们的代码,可能需要花上一个礼拜或更长的时间。 这里的问题是,我们在编写代码的时候不但需要考虑计算机 CPU 的想法,更要把以后 的开发者放在心上,除非,你写代码的唯一目的就是把它丢掉。 重构可以使得你的代码更容易理解。原因在于重构支持更小的类、更短的方法、更少的 局部变量、更小的系统耦合,重构要求你更加小心自己的命名机制,让名字反映出你的意图。 如果哪一块代码太复杂以至于难于理解,你都需要对它进行重构。 3,改善软件设计 大部分的软件工程师认为代码仅仅是设计的附属物。但我们必须正视的情况是,程序 设计在实现阶段完成的正是代码,而不是你脑袋里或纸上的设计。而绝大多数的故障也产生 于编码阶段。在当时或者以后找出这些故障被事实证明是非常昂贵的。 很多方法学强调把注意力放在分析和设计阶段,把实现定义为按照设计进行编码,以努 力防止产生故障。这些方法学认为通过分析和设计的严格化就能产生更高质量的软件。然而, 这仅仅是理论,实际上是不可能的。 另一方面,即使一开始的设计是完好的,随着用户对系统使用的深入,新的需求可能会 被加入,旧的需求会被修改、删除。设计不可能完全预料到这些变化。一旦实现开始偏离最 初的设计,那么它的代码将不受控制,从而不可避免地开始腐化(decay)。代码加入越多, 腐化的速度越快。如果没有办法让设计尽可能地与实现保持一致,那么这种腐化的最后结果 就是代码不得不被抛弃。 在传统的软件方法中,一旦开发到了实现阶段,就很难对设计做出变化。由于程序员编 写的代码倾向于腐化。它慢慢地偏离最初的设计,代码偏离最初的设计越远,反映设计的代 码就越难被看到,代码就越容易变坏。重构有助于把贬值的代码变得有用,并获得好的设计。 4,利于找出错误 软件调试中最困难和最费时间的工作是定位错误。重构不仅帮助理解代码,同时也可以 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 帮助发现错误。因为故障隐藏在系统中一些非常微妙的地方,清理设计和代码使故障被带到 明处。 重构使得程序代码的结构更清晰,每一个时刻可以更集中关注专一的数据和行为,这会 使你的工作量大大减少,效率大大提高。同时,由于重构要求小步前进,并且对每一步都进 行严格的测试,这样会使得错误很容易地浮现出来。所以重构期间进行的测试对于捕捉由于 测试不足而早期未被发现的故障是很有用的。 5,加速开发 重构的以上优点是明显的,重构能够加速开发的优点却不那么明显。人们甚至认为重构 会减慢开发速度。因为需要编写额外的单元测试,还要去重构那些本来就能够工作的代码。 事实上,重构确实能够加快软件的开发速度。一个好设计的基本前提就是允许更快的软 件开发。如果没有一个好的设计,一开始可能可以开发得很快,但是随着功能的增加,代码 逐渐腐化,结构渐渐失去。每次需要加入新功能时,必须花大量的时间去理解原来的代码, 修改原来代码的错误,改变一项功能需要花费越来越长的时间。 重构支持好的结构、设计和理解性,它让人们更快地开发软件。因为它可以防止软件的 腐化,甚至用于改进设计。 6,有助于代码复审 重构也能对代码复审做出贡献。它能被用于使复审的代码更容易理解,这种范围更广的 理解导致更有用的建议。当把这些建议作为展开重构的起点,由复审者即刻进行实现的话, 代码复审就能交付具体的结果。通过重构,重建设计的结构,代码变得更容易阅读,代码复 审变得更成功。 7,增加灵活性 在传统的开发中,需要的灵活性必须预设并体现在系统的设计中,这使得灵活性成为必 需付出较高代价的事情。如果使用重构技术,灵活性不再是开发过程中强加的约束。 重构降低初始设计的复杂程度。模式有其成本(间接性、复杂化),因此设计应该达到需 求所要求的灵活性,而不是越灵活越好。 如果在设计期间试图介入太多以后可能需要的灵活性,就会产生不必要的复杂和错误。 重构能够以多种方式扩展设计。他鼓励为手头的任务建立刚好适合的解决方案,当新的需求 来到时,可以通过重构扩展设计。 7.1.7 重构的不足和风险 1,重构的成本 重构的成本取决于所处的环境支不支持某些重构的基本原则。因为重构极大地依赖于每 一小步后的测试,因而拥有一套可靠的单元测试工具就能大量地降低手工测试成本。 应用重构涉及改变接口、名字、参数表等等,更新项目文档的成本不应低估。这就导致 整个系统设计的改变。由于在重构步骤之间老接口首先被保持,以后逐渐被新的所扩展,所 以,全部测试都不用改变,应该在整个重构期间内运行。之后当老的接口被取消,而支持新 接口时,测试也必须被更新。 2,重构的风险 由重构引起的软件失效可能具有严重的后果。因此不应该轻易地对待重构,必须小心 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 奕奕,并在在头脑中想着可能发生的问题。小步前进,每步后进行测试,以可预见的方式作 改变。 重新组织不要与添加新功能混在一起工作,这些重构原则使得不可能在重构时引入错 误。尽管如此,程序员不可能不犯错误。 因此在重构应用于系统之后,系统不仅应该通过必不可少的单元测试,而且也应该通过 回归功能测试,以表明重构后的系统与上一次的运行没有差别。即使在重构时引入了错误, 由于重构使系统具有良好结构,不带有重复代码,清晰的层次,以及小的单元,所以追踪这 些错误(一旦它们表现出来)将变得容易得多。 事实上,重构的一个重要的风险在于,需要一支高水平的程序员队伍,他们必须对重构 技术有深入的理解,而不至于使系统不受控的偏移到不希望的方向,这对过程控制的水平提 出了更高的要求。 7.2 设计模式 在模块设计阶段,最关键的问题是,用户需求是变化的,我们的设计如何适应这种变 化呢? 1,如果我们试图发现事情怎样变化,那我们将永远停留在分析阶段。 2,如果我们编写的软件能面向未来,那将永远处在设计阶段。 3,我们的时间和预算不允许我们面向未来设计软件。过分的分析和过分的设计,事实 上被称之为“分析瘫痪”。 如果我们预料到变化将要发生,而且也预料到将会在哪里发生。这样就形成了几个原则: 1,针对接口编程而不是针对实现编程。 2,优先使用对象组合,而不是类的继承。 3,考虑您的设计哪些是可变的,注意,不是考虑什么会迫使您的设计改变,而是考虑 要素变化的时候,不会引起重新设计。 也就是说,封装变化的概念是模块设计的主题。 解决这个问题,我们需要研究一下软件重构技术。而重构技术影响最大而且最成功的应 用,要数 GoF 的 23 种设计模式,在 GoF 中,把设计模式分为结构型、创建型和行为型三 大类,从不同的角度讨论了软件重构的方法。本课程假定学员已经熟悉这 23 个模式,因此 主要从设计的角度讨论如何正确选用恰当的设计模式。整个讨论依据三个原则: 1)开放-封闭原则 2)从场景进行设计的原则 3)包容变化的原则 下面的讨论会有一些代码例子,尽管在详细设计的时候,并不考虑代码实现的,但任 何架构设计思想如果没有代码实现做基础,将成为无木之本,所以后面的几个例子我们还是 把代码实现表示出来,举这些例子的目的并不是提供样板,而是希望更深入的描述想法。另 外,所用的例子大部分使用 Java 来编写,这主要因为希望表达比较简单,但这不是必要的, 可以用任何面向对象的语言(C#、C++)来讨论这些问题。 7.3 封装变化与面向接口编程 设计模式分为结构型、构造型和行为型三种问题域,我们来看一下行为型设计模式,行 为型设计模式的要点之一是“封装变化”,这类模式充分体现了面向对象的设计的抽象性。 在这类模式中,“动作”或者叫“行为”,被抽象后封装为对象或者为方法接口。通过这种抽 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 象,将使“动作”的对象和动作本身分开,从而达到降低耦合性的效果。这样一来,使行为 对象可以容易的被维护,而且可以通过类的继承实现扩展。 行为型模式大多数涉及两种对象,即封装可变化特征的新对象,和使用这些新对象的已 经有的对象。二者之间通过对象组合在一起工作。如果不使用这些模式,这些新对象的功能 就会变成这些已有对象的难以分割的一部分。因此,大多数行为型模式具有如下结构。 下面是上述结构的代码片断: public abstract class 行为类接口 { public abstract void 行为(); } public class 具体行为1:行为类接口 { public override void 行为() { } } public class 行为使用者 { public 行为类接口 我的行为; public 行为使用者() { 我的行为=new 具体行为1(); } public void 执行() { 我的行为.行为(); } } 7.4 合理使用外观和适配器模式 一、使用外观模式(Facade)使用户和子系统绝缘 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 外观模式为了一组子系统提供一个一致的方式对外交互。这样就可以使客户和子系统绝 缘,可以大大减少客户处理对象的数目,我们已经讨论过这个主题,这里就不再讨论了。 二、使用适配器模式(Adapter)调适接口 在系统之间集成的时候,最常见的问题是接口不一致,很多能满足功能的软件模块,由 于接口不同,而导致无法使用。 适配器模式的含义在于,把一个类的接口转换为另一个接口,使原本不兼容而不能一起 工作的类能够一起工作。适配器有类适配器和对象适配器两种类型,二者的意图相同,只是 实现的方法和适用的情况不同。类适配器采用继承的方法来实现,而对象适配器采用组合的 方法来实现。 1)类适配器 类适配器采用多重继承对一个接口与另一个接口进行匹配,其结构如下。 由于 Adapter 类继承了 Adaptee 类,通过接口的 Do()方法,我们可以使用 Adaptee 中的 Execute 方法。这种方式当语言不承认多重继承的时候就没有办法实现,这时可以使用对象 适配器。 2)对象适配器 对象适配器采用对象组合,通过引用一个类与另一个类的接口,来实现对多个类的适配。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 7.5 封装变化的三种方式及评价 设计可升级的架构,关键是要把模块中不变部分与预测可变部分分开,以防止升级过程 中对基本代码的干扰。这种分开可以有多种方式,一般来说可以从纵向、横向以及外围三个 方面考虑。 7.5.1 纵向处理:模板方法(Template Method) 1、意图 定义一个操作中的算法骨架,而将一些步骤延伸到子类中去,使得子类可以不改变一个 算法的结构,即可重新定义改算法的某些特定步骤。这里需要复用的使算法的结构,也就是 步骤,而步骤的实现可以在子类中完成。 2、使用场合 1)一次性实现一个算法的不变部分,并且将可变的行为留给子类来完成。 2)各子类公共的行为应该被提取出来并集中到一个公共父类中以避免代码的重复。首 先识别现有代码的不同之处,并且把不同部分分离为新的操作,最后,用一个调用这些新的 操作的模板方法来替换这些不同的代码。 3)控制子类的扩展。 3、结构 模板方法的结构如下: xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 在抽象类中定义模板方法的关键是: 在一个非抽象方法中调用调用抽象方法,而这些抽象方法在子类中具体实现。 代码: public abstract class Payment{ private double amount; public double getAmount(){ return amount; } public void setAmount(double value){ amount = value; } public String goSale(){ String x = "不变的流程一 "; x += Action(); //可变的流程 x += amount + ", 正在查询库存状态"; //属性和不变的流程二 return x; } public abstract String Action(); } class CashPayment extends Payment{ public String Action(){ return "现金支付"; } } 测试: public class Test{ public static void main (String[] args){ Payment o; xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 o = new CashPayment(); o.setAmount(555); System.out.println(o.goSale()); } } 假定系统已经投运,用户提出新的需求,要求加上信用卡支付和支票支付,可以这样写: public class CreditPayment extends Payment{ public String Action(){ return "信用卡支付,联系支付机构"; } } class CheckPayment extends Payment{ public String Action(){ return "支票支付,联系财务部门"; } } 调用: public class Test{ public static void main (String[] args){ Payment o; o = new CashPayment(); o.setAmount(555); System.out.println(o.goSale()); o = new CreditPayment(); o.setAmount(555); System.out.println(o.goSale()); o = new CheckPayment(); o.setAmount(555); System.out.println(o.goSale()); } } 7.5.2 简单工厂(Simpleness Factory)模式 1、意图 简单工厂的作用是实例化对象,而不需要客户了解这个对象属于哪个具体的子类。 在 GoF 的设计模式中并没有简单工厂,而是把它作为工厂方法的一个特例加以解释, 可以这样来理解,简单工厂是参数化的工厂方法,由于它可以处理粒度比较大的问题,所以 还是单独列出来比较有利。 2、使用场合和效果 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 简单工厂实例化的类具有相同的接口,类的个数有限而且基本上不需要扩展的时候,可 以使用简单工厂。 使用简单工厂的优点是用户可以根据参数获得类的实例,避免了直接实例化类的麻烦, 降低了系统的耦合度。缺点是实例化的类型在编译的时候已经确定,如果增加新的类,需要 修改工厂。 简单工厂需要知道所有要生成的类型,在子类过多或者子类层次过多的时候并不适合使 用。 3、结构 通常简单工厂不需要实例化,而是采用静态方法来实现。下面是一个简单工厂的基本框 架,Factory 类为工厂类,里面的静态方法 PaymentFactory 决定了实例化哪个子类,而在这 个方法里面,通过多分支语句来控制具体实现的子类。 // Payment.java public abstract class Payment{ private double amount; public double getAmount(){ return amount; } public void setAmount(double value){ amount = value; } public String goSale(){ String x = "不变的流程一 "; x += Action(); //可变的流程 x += amount + ", 正在查询库存状态"; //属性和不变的流程二 return x; } public abstract String Action(); xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 } class CashPayment extends Payment{ public String Action(){ return "现金支付"; } } // CreditPayment.java public class CreditPayment extends Payment{ public String Action(){ return "信用卡支付,联系支付机构"; } } class CheckPayment extends Payment{ public String Action(){ return "支票支付,联系财务部门"; } } //这是一个工厂类 Factory.java public class Factory{ public static Payment PaymentFactory(String PaymentName){ Payment mdb=null; if (PaymentName.equals("现金")) mdb=new CashPayment(); else if (PaymentName.equals("信用卡")) mdb=new CreditPayment(); else if (PaymentName.equals("支票")) mdb=new CheckPayment(); return mdb; } } 调用: public class Test{ public static void main (String[] args){ Payment o; o = Factory.PaymentFactory("现金"); o.setAmount(555); System.out.println(o.goSale()); o = Factory.PaymentFactory("信用卡"); o.setAmount(555); xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 System.out.println(o.goSale()); o = Factory.PaymentFactory("支票"); o.setAmount(555); System.out.println(o.goSale()); } } 7.5.3 横向处理:桥接模式(Bridge) 模板方法是利用继承来完成切割,当对耦合性要求比较高,无法使用继承的时候,可以 横向切割,也就是使用桥接模式。 桥接模式结构如下图。 其中: Abstraction:定义抽象类的接口,并维护 Implementor 接口的指针。 其内部有一个 OperationImp 实例方法,但使用 Implementor 接口的方法。 RefindAbstraction:扩充 Abstraction 类定义的接口,重要的是需要实例化 Implementor 接口。 Implementor:定义实现类的接口,关键是内部有一个 defaultMethod 方法,这个方法会 被 OperationImp 实例方法使用。 ConcreteImplementor:实现 Implementor 接口,这是定义它的具体实现。 我们通过上面的关于支付的简单例子可以说明它的原理。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 public class Payment{ private double amount; public double getAmount(){ return amount; } public void setAmount(double value){ amount = value; } private Implementor imp; public void setImp(Implementor s){ imp=s; } public String goSale(){ String x = "不变的流程一 "; x += imp.Action(); //可变的流程 x += amount + ", 正在查询库存状态"; //属性和不变的流程二 return x; } } interface Implementor { public String Action(); } class CashPayment implements Implementor{ public String Action(){ return "现金支付"; } } xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 调用: public class Test{ public static void main (String[] args){ Payment o=new Payment(); o.setImp(new CashPayment()); o.setAmount(555); System.out.println(o.goSale()); } } 假定系统投运以后,需要修改性能,可以直接加入新的类: public class CreditPayment implements Implementor{ public String Action(){ return "信用卡支付,联系支付机构"; } } class CheckPayment implements Implementor{ public String Action(){ return "支票支付,联系财务部门"; } } 调用: public class Test{ public static void main (String[] args){ Payment o=new Payment(); o.setImp(new CashPayment()); o.setAmount(555); System.out.println(o.goSale()); xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 o.setImp(new CreditPayment()); o.setAmount(555); System.out.println(o.goSale()); o.setImp(new CheckPayment()); o.setAmount(555); System.out.println(o.goSale()); } } 这样就减少了系统的耦合性。而在系统升级的时候,并不需要改变原来的代码。 7.5.4 核心和外围:装饰器模式(Decorator) 有的时候,希望实现一个基本的核心代码快,由外围代码实现专用性能的包装,最简单 的方法,是使用继承。 public abstract class Payment{ private double amount; public double getAmount(){ return amount; } public void setAmount(double value){ amount = value; } public String goSale(){ String x = "不变的流程一 "; x += Action(); //可变的流程 x += amount + ", 正在查询库存状态"; //属性和不变的流程二 return x; } public abstract String Action(); } xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 class CashPayment extends Payment{ public String Action(){ return "现金支付"; } } 实现: public class Test{ public static void main (String[] args){ Payment o; o = new CashPayment(); o.setAmount(555); System.out.println(o.goSale()); } } 如果需要调用 Action()方法以前,先显示一段文字,最简单的方法是加入继承: public class CashPayment1 extends CashPayment{ public String Action(){ //在执行原来的代码之前,先显示提示框 System.out.println("现金支付"); return super.Action(); } } 实现: public class Test{ public static void main (String[] args){ Payment o; xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 o = new CashPayment1(); o.setAmount(555); System.out.println(o.goSale()); } } 缺点: 继承层次多一层,提升了耦合性。 当实现类比较多的时候,实现起来就比较复杂。 在这样的情况下,也可以使用装饰器模式,这是用组合取代继承的一个很好的方式。 1、意图 事实上,上面所要解决的意图可以归结为“在不改变对象的前提下,动态增加它的功能”, 也就是说,我们不希望改变原有的类,或者采用创建子类的方式来增加功能,在这种情况下, 可以采用装饰模式。 2、结构 装饰器结构的一个重要的特点是,它继承于一个抽象类,但它又使用这个抽象类的聚合 (即即装饰类对象可以包含抽象类对象),恰当的设计,可以达到我们提出来的目的。 模式中的参与者如下: Component (组成):定义一个对象接口,可以动态添加这些对象的功能,其中包括 Operation(业务)方法。 ConcreteComponent(具体组成):定义一个对象,可以为它添加一些功能。 Decorator(装饰):维持一个对 Component 对象的引用,并定义与 Component 接口一致的 接口。 ConcreteDecorator(具体装饰):为组件添加功能。它可能包括 AddedBehavior(更多的行 为)和 AddedState(更多的状态)。 举个例子: 假定我们已经构造了一个基于支付的简单工厂模式的系统。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 现在需要每个类在调用方法 goSale()的时候,除了完成原来的功能以外,先弹出一个对 话框,显示工厂的名称,而且不需要改变来的系统,为此,在工厂类的模块种添加一个装饰 类 Decorator,同时略微的改写一下工厂类的代码。 //装饰类 public class Decorator extends Payment{ private String strName; public Decorator(String strName){ this.strName = strName; } private Payment pm; public void setPm(Payment value){ pm = value; } public String Action(){ //在执行原来的代码之前,显示提示框 System.out.println(strName); return pm.Action(); } } 而工厂类: //这是一个工厂类 public class Factory{ public static Payment PaymentFactory(String PaymentName){ Payment mdb=null; if (PaymentName.equals("现金")) mdb=new CashPayment(); else if (PaymentName.equals("信用卡")) mdb=new CreditPayment(); xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 else if (PaymentName.equals("支票")) mdb=new CheckPayment(); //return mdb; Decorator m=new Decorator(PaymentName); m.setPm(mdb); return m; } } 可以说,这是在用户不知晓的情况下,也不更改原来的类的情况下,改变了性能。 7.6 利用观察者模式延长架构的生命周期 当需要上层对底层的操作的时候,可以使用观察者模式实现向上协作。也就是上层响 应底层的事件,但这个事件的执行代码由上层提供。 1、意图: 定义对象一对多的依赖关系,当一个对象发生变化的时候,所有依赖它的对象都得到通 知并且被自动更新。 2、结构 传统的观察者模式结构如下。 3,举例: // PaymentEvent.java 传入数据的类 import java.util.*; public class PaymentEvent extends EventObject { xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 private String Text; //定义 Text 内部变量 //构造函数 public PaymentEvent(Object source,String Text) { super(source); this.Text=Text; //接收从外部传入的变量 } public String getText(){ return Text; //让外部方法获取用户输入字符串 } } //监听器接口 //PaymentListener.java import java.util.*; public interface PaymentListener extends EventListener { public String Action(PaymentEvent e); } // Payment.java 平台类 import java.util.*; public class Payment{ private double amount; public double getAmount(){ return amount; } public void set(double value){ amount = value; } //事件编程 private ArrayList elv; //事件侦听列表对象 //增加事件事件侦听器 public void addPaymentListener(PaymentListener m) { //如果表是空的,先建立它的对象 if (elv==null){ elv=new ArrayList(); } //如果这个侦听不存在,则添加它 if (!elv.contains(m)){ xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 elv.add(m); } } //删除事件侦听器 public void removePaymentListener(PaymentListener m) { if (elv!= null && elv.contains(m)) { elv.remove(m); } } //点火 ReadText 方法 protected String fireAction(PaymentEvent e) { String m=e.getText(); if (elv != null) { //激活每一个侦听器的 WriteTextEvett 事件 for (int i = 0; i < elv.size(); i++) { PaymentListener s=(PaymentListener)elv.get(i); m+=s.Action(e); } } return m; } public String goSale(){ String x = "不变的流程一 "; PaymentEvent m=new PaymentEvent(this,x); x = fireAction(m); //可变的流 x += amount + ", 正在查询库存状态"; //属性和不变的流程二 return x; } } 调用:Test.java //Test.java import java.util.*; public class Test { public Test(){ } //入口 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 public static void main(String[] args) { Payment o1 = new Payment(); Payment o2 = new Payment(); Payment o3 = new Payment(); o1.addPaymentListener(new PaymentListener(){ public String Action(PaymentEvent e){ return e.getText()+" 现金支付 "; } }); o2.addPaymentListener(new PaymentListener(){ public String Action(PaymentEvent e){ return e.getText()+" 信用卡支付 "; } }); o3.addPaymentListener(new PaymentListener(){ public String Action(PaymentEvent e) { return e.getText()+" 支票支付 "; } }); o1.set(777); o2.set(777); o3.set(777); System.out.println(o1.goSale()); System.out.println(o2.goSale()); System.out.println(o3.goSale()); } } 7.7 利用策略与工厂模式实现通用的框架 一、应用策略模式提升层的通用性 1、意图 将算法封装,使系统可以更换或扩展算法,策略模式的关键是所有子类的目标一致。 2、结构 策略模式的结构如下。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 其中:Strategy(策略):抽象类,定义需要支持的算法接口,策略由上下文接口调用。 二、示例:利用反射实现通用框架 目标: 构造一个 Bean 容器框架,可以动态装入和构造对象,装入类可以使用配置文件,这里 利用了反射技术。 问题: 如何动态构造对象,集中管理对象。 解决方案: 策略模式,XML 文档读入,反射的应用。 我们现在要处理的架构如下: Bean.xml 文档的结构如下。 说明 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 内容 ……….. 应用程序上下文接口:ApplicationContext.java 只有一个方法,也就是由用户提供的 id 提供 Bean 的实例。 package springdemo; public interface ApplicationContext{ public Object getBean(String id) throws Exception; } 上下文实现类:FileSystemXmlApplicationContext.java package springdemo; import java.util.*; import javax.xml.parsers.*; import org.w3c.dom.*; import java.io.*; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.*; public class FileSystemXmlApplicationContext implements ApplicationContext{ //用一个哈西表保留从 XML 读来的数据 private Hashtable hs=new Hashtable(); public FileSystemXmlApplicationContext(String fileName){ try{ readXml(fileName); } catch(Exception e){ e.printStackTrace(); } } //私有的读 XML 方法。 private void readXml(String fileName) throws Exception{ //读 XML 把数据放入哈西表 hs=Configuration.Attribute(fileName,"bean","property"); xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 } public Object getBean(String id) throws Exception{ //由 id 取出内部的哈西表对象 Hashtable hsb=(Hashtable)hs.get(id); //利用反射动态构造对象 Object obj =Class.forName(hsb.get("class").toString()).newInstance(); java.util.Enumeration hsNames1 =hsb.keys(); //利用反射写入属性的值 while (hsNames1.hasMoreElements()) { //写入利用 Set 方法 String ka=(String)hsNames1.nextElement(); if (! ka.equals("class")){ //写入属性值为字符串 String m1="String"; Class[] a1={m1.getClass()}; //拼接方法的名字 String sa1=ka.substring(0,1).toUpperCase(); sa1="set"+sa1+ka.substring(1); //动态调用方法 java.lang.reflect.Method fm=obj.getClass().getMethod(sa1,a1); Object[] a2={hsb.get(ka)}; //通过 set 方法写入属性 fm.invoke(obj,a2); } } return obj; } } //这是一个专门用于读配置文件的类 class Configuration{ public static Hashtable Attribute(String configname, String mostlyelem, String childmostlyelem) throws Exception{ Hashtable hs=new Hashtable(); //建立文档,需要一个工厂 DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance(); DocumentBuilder builder=factory.newDocumentBuilder(); Document doc=builder.parse(configname); //建立所有元素的列表 Element root = doc.getDocumentElement(); //把所有的主要标记都找出来放到节点列表中 NodeList elemList = root.getElementsByTagName(mostlyelem); for (int i=0; i < elemList.getLength(); i++){ xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 //获取这个节点的属性集合 NamedNodeMap ac = elemList.item(i).getAttributes(); //构造一个表,记录属性和类的名字 Hashtable hs1=new Hashtable(); hs1.put("class",ac.getNamedItem("class").getNodeValue()); //获取二级标记子节点 Element node=(Element)elemList.item(i); //获取第二级节点的集合 NodeList elemList1 =node.getElementsByTagName(childmostlyelem); for (int j=0; j < elemList1.getLength(); j++){ //获取这个节点的属性集合 NamedNodeMap ac1 = elemList1.item(j).getAttributes(); String key=ac1.getNamedItem("name").getNodeValue(); NodeList node1=((Element)elemList1.item(j)).getElementsByTagName("value"); String value=node1.item(0).getFirstChild().getNodeValue(); hs1.put(key,value); } hs.put(ac.getNamedItem("id").getNodeValue(),hs1); } return hs; } } 做一个程序实验一下。 首先做一个关于交通工具的接口:Vehicle.java package springdemo; public interface Vehicle { public String execute(String str); public String getMessage(); public void setMessage(String str); } 做一个实现类:Car.java package springdemo; public class Car implements Vehicle { private String message=""; private String x; public String getMessage(){ xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 return message; } public void setMessage(String str){ message = str; } public String execute(String str){ return getMessage() + str+"汽车在公路上开"; } } Bean.xml 文档。 Spring Quick Start hello! 测试:Test.java public class Test{ public static void main (String[] args) throws Exception{ springdemo.ApplicationContext m= new springdemo.FileSystemXmlApplicationContext("d:\\Bean.xml"); //实现类,使用标记 Car springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car"); System.out.println(s1.execute("我的")); } } 基于接口编程将使系统具备很好的扩充性。 再做一个类:Train.java package springdemo; public class Train implements Vehicle { private String message=""; public String getMessage(){ return message; } xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 public void setMessage(String str){ message = str; } public String execute(String str) { return getMessage() + str+"火车在铁路上走"; } } Bean.xml 改动如下。 Spring Quick Start hello! haha! 改动一下 Test.java public class Test{ public static void main (String[] args) throws Exception{ springdemo.ApplicationContext m= new springdemo.FileSystemXmlApplicationContext("d:\\Bean.xml"); //实现类,使用标记 Car springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car"); System.out.println(s1.execute("我的")); springdemo.Vehicle s2=(springdemo.Vehicle)m.getBean("Train"); System.out.println(s2.execute("你的")); } } 我们发现,在加入新的类的时候,使用方法几乎不变。 再做一组不同的接口和类来看一看:IDemo.java package springdemo; public interface IDemo { xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 public void setX(String x); public void setY(String y); public double Sum(); } 实现类:Demo.java package springdemo; public class Demo implements IDemo { private String x; private String y; public void setX(String x){ this.x = x; } public void setY(String y) { this.y = y; } public double Sum(){ return Double.parseDouble(x)+Double.parseDouble(y); } } Bean.xml 改动如下: Spring Quick Start hello! haha! 20 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 30 改写 Test.java public class Test{ public static void main (String[] args) throws Exception{ springdemo.ApplicationContext m= new springdemo.FileSystemXmlApplicationContext("d:\\Bean.xml"); //实现类,使用标记 Car springdemo.Vehicle s1=(springdemo.Vehicle)m.getBean("Car"); System.out.println(s1.execute("我的")); springdemo.Vehicle s2=(springdemo.Vehicle)m.getBean("Train"); System.out.println(s2.execute("你的")); springdemo.IDemo s3=(springdemo.IDemo)m.getBean("demo"); System.out.println(s3.Sum()); } } 通过上面的讨论,我们当对框架实现技术有了一个基本的理解。 7.8 单件模式的应用问题 有时候,我们需要一个全局唯一的连接对象,这个对象可以管理多个通信会话,在使用 这个对象的时候,不关心它是否实例化及其内部如何调度,这种情况很多,例如串口通信和 数据库访问对象等等,这些情况都可以采用单件模式。 1、意图 单件模式保证应用只有一个全局唯一的实例,并且提供一个访问它的全局访问点。 2、使用场合 当类只能有一个实例存在,并且可以在全局访问的时候,这个唯一的实例应该可以通过 子类实现扩展,而且用户无需更改代码即可以使用。 3、结构 单件模式的结构非常简单,包括防止其它对象创建实例的私有构造函数,保持唯一实例 的私有变量和全局变量访问接口等,请看下面的例子: class CShapeSingletion { private static CShapeSingletion mySingletion=null; //为了防止用户实例化对象,这里把构造函数设为私有的 private CShapeSingletion() {} xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 //这个方法是调用的入口 public static CShapeSingletion ShapeInstance() { if (mySingletion==null) { mySingletion=new CShapeSingletion(); } return mySingletion; } private int intCount=0; //计数器,虽然是实例方法,但这里的表现类同静态 public int Count() { intCount+=1; return intCount; } } //调用代码: public class MySingletion { public static void main (String[] args) { CShapeSingletion m1; CShapeSingletion m2; CShapeSingletion m3; CShapeSingletion m4; m1=CShapeSingletion.ShapeInstance(); m2=CShapeSingletion.ShapeInstance(); m3=CShapeSingletion.ShapeInstance(); m4=CShapeSingletion.ShapeInstance(); System.out.println(m1.Count()); System.out.println(m2.Count()); System.out.println(m3.Count()); System.out.println(m4.Count()); } } 4、效果 单件提供了全局唯一的访问入口,因此比较容易控制可能发生的冲突。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 单件是对静态函数的一种改进,首先避免了全局变量对系统的污染,其次它可以有子类, 业可以定义虚函数,因此它具有多态性,而类中的静态方法是不能定义成虚函数的。 单件模式也可以定义成多件,即允许有多个受控的实例存在。 单件模式维护了自身的实例化,在使用的时候是安全的,一个全局对象无法避免创建多 个实例,系统资源会被大量占用,更糟糕的是会出现逻辑问题,当访问象串口这样的资源的 时候,会发生冲突。 5、单件与实用类中的静态方法 实用类提供了系统公用的静态方法,并且也经常采用私有的构造函数,和单件不同,它 没有实例,其中的方法都是静态方法。 实用类和单件的区别如下: 1)实用类不保留状态,仅提供功能。 2)实用类不提供多态性,而单件可以有子类。 3)单件是对象,而实用类只是方法的集合。 应该说在实际应用中,实用类的应用更加广泛,但是在涉及对象的情况下需要使用单件, 例如,能不能用实用类代替抽象工厂呢?如果用传统的方式显然不行,因为实用类没有多态 性,会导致每个工厂的接口不同,在这个情况下,必须把工厂对象作为单件。 因此何时使用单件,要具体情况具体分析,不能一概而论。 7.9 代理模式的应用 一、代理模式简述 代理模式的意图,是为其它对象提供一个代理,以控制对这个对象的访问。 首先作为代理对象必须与被代理对象有相同的接口,换句话说,用户不能因为使不使用 代理而做改变。其次,需要通过代理控制对对象的访问,这时,对于不需要代理的客户,被 代理对象应该是不透明的,否则谈不上代理。下图是代理模式的结构。 二、案例:在团队并行开发中使用代理模式 软件开发需要协同工作,希望开发进度能够得到保证,为此需要合理划分软件,每个成 员完成自己的模块,为同伴留下相应的接口。 在开发过程中,需要不断的测试。然而,由于软件模块之间需要相互调用,对某一 模块的测试,又需要其它模块的配合。而且在模块开发过程中也不可能完全同步,从而 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 给测试带来了问题。 假定在我们设计的订单处理子系统中, Ordre(订单)类在计算订购项目金额总合 的时候,需要由客户服务子系统根据客户级别和销售策略来计算实际收取的费用,而客 户服务系统由 OrderItme(订单项)类提供这个服务,显然这是由另一个开发组完成。 其中:Ordre 包括若干 OrderItme,订单的总价是每个订单项之和。 在两个开发组共同完成这个项目的情况下,如果 OrderItme 没有完成,Ordre 也就没有 办法测试。一个简单的办法,是 Ordre 开发的时候屏蔽 OrderItme 调用,但这样代码完成的 时候需要做大量的垃圾清理工作,显然这是不合适的,我们的问题是,如何把测试代码和实 际代码分开,这样更便于测试,而且可以很好的集成。 如果我们把 OrderItem 抽象为一个接口或一个抽象类,实现部分有两个平行的子类,一 个是真正的 OrderItem,另一个是供测试用的 TestOrderItem,在这个类中编写测试代码,我 们称之为 Mock。 这时,Order 可以使用 TestOrderItem,测试。当 OrderItem 完成以后,有需要使用 OrderItem 进行集成测试,如果 OrderItem 还要修改,又需要转回 TestOrderItem。 我们希望只用一个参数就可以完成这种切换,比如在配置文件中,测试设为 true,而正 常使用为 false。 这些需求牵涉到代理模式的应用,现在可以把代理结构画清楚。 这就很好的解决了问题。 实例: 在配置文件 Bean.xml 中加一个元素: xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 编写抽象的定单类: package demo; //这是统一的接口 public abstract class AbstractOrderItem{ private String goodName; private long count; private double price; public void setGoodName(String m_GoodName){ goodName=m_GoodName; } public String getGoodName(){ return goodName; } public void setCount(long m_Count){ count=m_Count; } public long getCount(){ return count; } public void setPrice(double m_Price){ price=m_Price; } public double getPrice(){ return price; } //价格求和,这个计算方式是另外的人编写的 public abstract double GetTotalPrice() throws Exception; } 编写订单代码: package demo; //处理订单代码 public class Order{ public String Name; //public DateTime OrderDate; private java.util.ArrayList oitems; public Order(){ oitems=new java.util.ArrayList(); xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 } public void AddItem(AbstractOrderItem it){ oitems.add(it); } public void RemoveItem(AbstractOrderItem it){ oitems.remove(it); } public double OrderPrice() throws Exception{ AbstractOrderItem it; double op=0; for (int i=0;i,其中,x 和 y 是形成弧两端的结 点,方向是从 x 到 y。 入度(in-degree):到达一个节点的弧的数量。 出度(out-degree):离开一个节点的弧的数量。 路径(path):是指前后连续的有向边序列,在这个序列中,经过某些边的次数可能不 止一次。 简单路径(simple path):指不会出现重复边的路径。 例如:在上面的例子里,结点 70 的入度是 1,出度是 2。 流图(flow graph):所谓流图是指这样一种图,即图中有两个结点,开始结点(start node) 和终止节点(stop node),开始节点入度为零,终止结点出度为零,每个结点都位于从开始 节点到终止结点的某条路径上。 过程节点(procedure node):出度等于 1 的结点成为过程节点。 谓词节点(predicate node):除过程节点之外的所有节点(除终止节点之外)。 我们为程序结构建模的过程中,有些流图是经常见到的,有必要对它进行特殊的命名。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 上述的流图有个很重要的共同性质,就是它可以当作结构化程序的“积木”来使用,为 了理解这个问题,必须对构建流图的各种方式进行形式化定义。 1,循序与嵌套 我们只能根据两种合法操作来对旧的流图构建新的流图,循序连接(seqnencing)和嵌 套(nesting),在程序结构方面,这两种操作都有合法的解释。 顺序连接操作: 设 F1 和 F2 是两个流图,则 F1 与 F2 的顺序连接操作,是把 F1 的终止点与 F2 的开始 点合并所形成的流图。 记为(可选任何一种方式): F1;F2 或 Seq(F1,F2) 或 P2(F1,F2) 下面的例子,为流图 D1 和 D3 的顺序连接操作。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 嵌套连接操作: 设 F1 与 F2 是两个流图,F1 有一个过程节点 X,则,F2 在 X 点嵌套于 F1 上,是指在 F1 的基础上,通过用整个 F2 替换从 x 开始的弧而形成的流图,我们把这个结果记为: F1(F2 on X) 在确信不会对图中节点产生混淆的情况下,也可以记为: F1(F2) 下图显示的是 D3 嵌套于 D1 的情况。 由于本课程的许多例子,嵌套的实际节点是无关紧要的,所以一般记为: F(F1,F2,…Fn) 下图显示了流图的构造过程,这个过程进行了多次嵌套和顺序连接操作。 素流图(prime flowgraph): 是指不能用顺序连接或者嵌套来进行有用分解的流图。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 在上面的例子中,合并前的图都是素流图,而合并后的图都不是素流图,因为可以分解。 2,结构化的一般概念 结构化程序设计的定义:如果程序的结构只是由三种情况组成,即顺序(sequence)、 选择(selection)和迭代(iteration),则称这个程序是结构化的。这个原理可以追朔到 Böhm 和 Jacopini 于 1966 年提出来的经典理论。这个理论表明,任何算法都可以用三种结构来实 现。 后来有些人士曾经不太恰当的认为,这个结论无异是在说,只要程序中不出现 goto 语 句,这个程序就是结构化的,今天的事实证明这并不恰当,因为无论面向过程还是面向对象 的语言,goto 语句还是作为一个重要的成员存在,在构造一些特殊的结构的时候,还在发挥 作用。尽管一般来说,goto 语句还是少用为好。 我们希望对某个独立的程序评估,以确定它是否结构化的,最原始的定义对解决这个问 题帮助并不大,我们需要一个能够确定任意流图的结构化等级的机制。 为此,我们需要引入更多的、与素流图的图族 S(结构化)相关的术语。 如果图族满足下面的递归规则,则可以说这个图族是 S 结构化的(S-structured),或者 更简单的说,图族中的所有元素都是 S 图(S-graph)。 1,所有的元素都是 S 结构化的。 2,如果 F 和 F’都是 S 结构化的流图,则下列各项亦然: a) F;F’ b) F(F’) 只要对 F’嵌套于 F 进行了定义。 3,只有通过对上述步骤有限次的应用生成的图才是 S 结构化的。 前面我们关于基本结构化流图的说明,已经定义了几个元素符号 Pn(顺序结构)、D。 (if-then 分支)、D1(if-then-else 分支)、 Cn(case 分支)、D2(while 循环)、D3(repeat 循环),这些元素称之为基本 S 图(basic S-graph),我们可以自由的使用者写基本单元来构 造结构化单元。 在结构化程序设计文献中,定义: SD={P1,D0,D2} 这个定义称之为D结构化(D-structured),Böhm和Jacopini的研究曾表明,所有的算法 都可以编码为SD图,不过现在还是把比D结构化更大的集合作为S结构化的集合,也就是S 结构化单元是集合: S={P1,P2,….D1,D2,….C1,C2…..} 3,素分解 Fenton 和 Whitty 的研究表明,我们可以把任意一个流图与分解树(decomposition tree) 相联系,来描述如何通过素流图进行顺序连接和嵌套操作来构建图的。 下图是根据给定的流图来确定分解树的简单例子。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 研究表明,我们不但总是能够把流图分解成素流图,而且还能确信这种分解始终是唯一 的。这个结论被称之为素分解定律(prime decomposition theorem)。 唯一定义的素分解树,是对程序控制结构的一种定义性描述。 当然,对于一个比较大的流图来说,实现素分解并不是容易的事情,手工完成这种计算 也是不切实际的,好在许多商业软件能够自动完成这项工作。 素分解的唯一性定律,为我们研究软件结构复杂性度量提供了一个手段,一般来说,程 序包含的素流图越多,素流图的嵌套越深,程序结构往往也就越复杂。 7.11.2 结构复杂性度量 一、软件复杂性及度量原则 软件复杂性度量: 开发规模相同、复杂性不同的软件,花费的时间和成本会有很大差异。我们可以从六个 方面描述软件复杂性 ①理解程序的难度; ②纠错、维护程序的难度; ③向他人解释程序的难度; ④按指定方法修改程序的难度; ⑤根据设计文件编写程序的工作量; ⑥执行程序时需要资源的程度。 它反映了软件的可理解性、模块性、简洁性等属性。 软件复杂性度量的原则: 1.软件复杂性与程序大小的关系不是线性的; 2.控制结构复杂的程序较复杂; 3.数据结构复杂的程序较复杂; 4.转向语句使用不当的程序较复杂; 5.循环结构比选择结构复杂;选择结构又比顺序结构复杂; xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 6.语句、数据、子程序和模块在程序中的次序对复杂性有影响; 7.全程变量、非局部变量较多时,程序较复杂; 8.参数按地址调用比按值调用复杂; 9.函数副作用比显式参数传递难于理解; 10.具有不同作用的变量共用一个名字时较难理解; 11.模块间、过程间联系密切的程序比较复杂; 12.嵌套越深程序越复杂。 二、层次化度量 我们已经知道,唯一定义的素分解树,是对程序控制结构的一种定义性描述。所以我们 就可以对素流图、顺序连接以及嵌套操作所产生的影响进行复杂性度量方面的研究。例: 假定我们需要正式测量“嵌套深度”这个直观的概念。假设我们已经用流图 F 对程序 结构建模,想要计算 F 的嵌套深度α。 我们可以用素流图、顺序连接和嵌套来表示α。 讨论如下: 素流图:素流图 P1 的嵌套深度等于 0,其他任何素流图的嵌套深度等于 1。 所以,α(P1)=0 。如果 F 是一个不等于 P1 的素流图,则α(F)=1。 顺序连接:顺序连接 F1,...Fn 的嵌套深度等于 Fi 中最大嵌套深度值。 所以: α(F1;F2;...;Fn)=max(α(F1), α(F2), ..., α(Fn)) 嵌套:流图 F(F1;F2;...;Fn)的嵌套深度,等于 Fi 中的最大嵌套深度值加一,因为 F 本身就存在一个嵌套层次。所以: α(F(F1;F2;...;Fn))=1+max(α(F1), α(F2), ..., α(Fn)) 例如,我们来考虑如下图所示的结构流程,和它的结构树。 我们已经知道结构树的表达式为: F=D1((D0;P1,D2),D0(D3)) 所以可以计算: α(F)= α(D1((D0;P1,D2),D0(D3))) =1+max(α(D0;P1,D2), α(D0(D3))) {嵌套规则} =1+max(max(α(D0),α(P1),α(D2),1+α(D3)) {顺序连接规则和嵌套规则} xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 =1+max(max(1,0,1),2) =3 直观地看,嵌套深度也是 3。 这种把素流图、顺序规则和嵌套规则分层处理的方式,使我们可以定义一个度量 m,称 之为层次化度量(hierarchical measure)。 1,素分解规则:实际上是对要研究问题的定义,把这条规则命名为 M1。 2,顺序连接函数:把这条规则命名为 M2. 3,嵌套函数 h:把这条规则命名为 M3. 例:我们来研究一下由结构树得到代码行长度 v 的清晰定义。 M1:v(P1)=1,而且每个 F≠P1 的素流图(不是顺序结构),都有 v(F)=n+1,其中 n 是 F 中过程节点的数量。 说明: 过程结点的长度(它通常和没有控制流的语句相对应)等于 1,例如: double Flag = P / div - Math.Abs(P / div); 有 n 个控制结点的素流图长度(它通常包含有 n 个控制语句的控制语句)等于 n+1,例 如: if (P<10) { textBox1.Text = P.ToString(); div += 1; } 在这个例子中,直观地可以数出来:n=2 ,总行数为 3。 这和我们实际的映像是一致的。 M2:v(F1;F2;…;Fn)=∑v(Fi) ,顺序结构的语句数等于每个结构的语句数之和。 M3::v(F(F1,F2,…Fn))=1+∑v(Fi) , 对于每个 F≠P1 的素流图。嵌套结构的语句数,等 于每个嵌套单元的语句数之和加 1。 后两个表达应该不会引起争议。还是用上面的例子: v(F)=v(D1((D0;P1;D2),D0(D3))) =1+(v(D0;P1;D3)+V(D0(D3))) =1+(v(D0)+v(P1)+v(D2)+(!+v(D3)) =1+(3+1+2)+(1+2) =10 由这个原理,我们可以得到一系列的能够捕获具体特征的、简单但又重要的层次化度量, 从某种意义上说,这些度量测量的都是“复杂性”。例如: 结点数度量 n: M1:对于任何一个素流图 F,都有 n(F)=F 中的结点数。 M2:n(F1;F2;…..Fn)= ∑n(Fi)-k+1 M3:对于任何一个素流图 F,都有: n(F(F1,F2,…Fn)=n(F)+ ∑n(Fi)-2k 边数度量 e: M1:对于任何一个素流图 F,都有 e(F)=F 中的边数。 M2:e(F1;F2;…..Fn)= ∑e(Fi) M3:对于任何一个素流图 F,都有: xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 e(F(F1,F2,…Fn)=e(F)+ ∑e(Fi)-n “最大素流图”度量 k: M1:对于任何一个素流图 F,都有 k(F)=F 中的谓词数。 M2:k(F1;F2;…..Fn)= max(k(F1),k(F2),...k(Fn) M3:对于任何一个素流图 F,都有: k(F(F1,F2,…Fn)=max(k(F),k(F1),…k(Fn)) 还有一些度量就不再列出了。 三、McCabe 圈复杂性度量 不管怎么说,直接使用素分解进行复杂性度量还是比较困难的,使用中还是希望找到一 种简单直接的方法来解决这个问题,另外,人们希望找到一种方法,把边数、结点数以及素 流图数综合在一起考虑,McCabe 圈复杂性度量为我们解决这个问题提供了可能。 McCabe 圈复杂性度量是对层次化度量深入研究的结果,并且提供了一个简单易行的表 达式,下面我们来研究一下这个问题。 我们已经详细的研究了控制流图(或称程序控制结构图),我们知道: 程序结构对应于有一个入口结点和一个出口结点的有向图。 图中每个结点对应一个语句或一个顺序流程的程序代码块。 弧对应于程序中的转移。 由入口结点可以到达图中每个结点,并且从图中每个结点都可以到达出口结点。 McCabe 用程序控制结构图的圈数(巡回秩数)V(G)作为程序结构复杂性的度量 V(F) = e-n+2 其中:e 为结构图的边数  n为结构图的结点数 可以证明 V(G)等于结构图中有界或无界的封闭区域个数 例:计算如图所示程序控制结构图的 V(F)值。 (1) e=1,n=2,v=1; (2) e=3,n=3,v=2; xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 (3) e=4,n=4,v=2; (4) e=3,n=3,v=2; (5) e=6,n=5,v=3. McCabe 建议把 V(F)作为模块规模的定量指标,一个模块 V(F)的值不要大于 10 当 V(F)>10 时,模块内部结构就会变得复杂,给编码和测试带来困难。 程序中分枝结构数和循环结构数增加时,控制结构图的区域数增加,V(F)的值增大,程 序的结构会变得更复杂。 结构化程序设计控制流力求从高层指向低层,从低层指向高层的流向,会增加封闭区域 的个数,反方向的控制流向越多 V(F)越大,程序结构越复杂。 McCabe 圈数结构复杂度度量使用起来非常方便,在实践中也有很好的应用。 从测量理论的角度,让人十分怀疑的是,这个假设与复杂性的的直观关系是相对应的 吗?我们无法说明这个度量就是复杂性度量。但是,用圈数确定程序或者模块的测试和维护 难度的有用的指示器是非常有效的。在这个背景下,V(F)可以用于对产品的质量保证。 例:Grady 曾经报告过 HP 公司的一个研究项目,对 850000 行 FORTRAN 代码的每个 模块计算了圈数。研究人员发现,在模块圈数和需要更新的次数之间存在着紧密联系。在对 更新次数超过三次的模块上造成的成本和进度影响进行研究以后,研究小组得出结论,模块 中允许的最大圈数为 15。 例:Channel Tunnel 铁路系统中的软件质量保证规程要求:如果 Logiscope 工具发现模 块的圈数超过 20,或者模块的语句数超过 50,就会认为不合格。 7.12 软件架构挖掘 仔细研究软件架构设计的方法,我们可以发现,软件架构设计是一个知识积累的过程, 为了有效的增加知识积累的能力,我们可以利用一个附加实践,那就是所谓架构挖掘。 7.12.1 架构挖掘过程 1,自顶向下和自底向上 自顶向下的设计方法,强调的是把设计视图或者需求文档这样一些抽象的概念,进一步 转化为具体的设计和实现。这事实上是一种预先方法,在实现之前必须产生设计计划。 而在自底向上的设计方法,一个新的设计是从基本的程序或有关部分开始创建的,在递 增变化以设计复用方面,往往更具备生产效率。自底向上方法实际上是一种事后方法,文档 往往是根据已经建立的结构完成的。 一般来说我们推荐自顶向下方法,它体现了一个系统有计划的开发,这样更便于构建有 效的系统。但是,不论愿不愿意,系统当初的设计是不可能一成不变的,这是一个螺旋上升 的迭代过程,在这个过程中,架构师会对应用问题有更深入的理解,不断创建许多新的设计。 自底向上的方法可以认为是这个主体过程的一个补充,事实上大多数信息系统都存在预 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 先设计,有些设计存在于已经完成的系统中,利用这个信息,架构师就可以在前期建立有效 的原型,对这些信息进行抽取,把抽取的结果应用于软件架构的过程,称之为架构挖掘。 架构挖掘是利用现存设计与实践经验来创建新的架构,通过回顾大量的实现细节,发现、 抽取、提炼设计知识。 2,架构挖掘过程 架构挖掘开始之前,首先需要识别一组与设计问题相关的代表性技术,这些技术的搜寻 可以用各种方法进行(拜访专家、技术研讨会、网上搜索等),也可以由针对性地进行技术 预研。 第一步:对典型技术建模,产生相关的软件接口规范。 第二步:已经挖掘的设计被一般化用于创建一个共同的接口规范。 在这一步,我们不是要得到一个最简约的典型设计,而是应该有一个更加良好的解决方 案。 第三步:提炼设计,提炼的驱动力:架构师的评定、非正式走查、回顾过程、新的需求、 其它挖掘研究等。 7.12.2 架构挖掘的方法学问题 1,挖掘的适用性 挖掘的目的事实上并不是具体产品,事实上挖掘的真正目标是架构师的启迪。这对于降 低风险和提高架构的质量是有明显好处的。如果对问题和先前的解决方案能有成熟的理解, 架构师就可以着手准备架构设计了。 挖掘的另一个结果是接口模型,尽管这不是正规的产品,但可以应用于架构的创造性设 计的方法中。挖掘工作对于高风险的或者有着广泛影响的重大设计,是很有意义的,它可以 提高设计的质量和复用性。一般来说,一个典型技术的挖掘研究几天就可以完成,经过几个 挖掘研究之后,就可以满怀信心地开始设计了。 架构师的知识主要来自于多年的实践积累,如果我们不注意把这种积累充分挖掘出来, 总是针对每个新项目重新开始,往往产生一些不成熟的、习惯性的设计,这就增加了设计的 风险。 2,水平与垂直设计元素 在设计模式的讨论中,我们知道建立一个在未来可以适应变化的系统从技术上是可行 的,这种设计被称之为水平设计元素,它的特点是重点考虑软件复用。而针对一个唯一的需 求实现硬编码,被称之为垂直设计元素,它的特点是只考虑具体实现。 架构设计的一个重要目标,就是在水平与垂直设计元素的考虑上寻求平衡,这是一个往 往让人感到迷惑的问题。作为程序员一般比较偏好垂直设计,因为这可以使用一种直接的方 式编码,他们的思维是:既然这样就能解决问题,为什么还要采用其它的方式呢? 但作为构架师,就需要关注具有长期影响的其它设计要点。经验告诉我们,需求的变化 是频繁的,而我们也知道了一些设计方法可以灵活的适应这些变化。这样,我们就可以采用 某种能够合理管理变化的方法,方法是: 1)列出可能的变化源以及它们对设计的影响,这称之为“架构评估”。 2)研究哪些局部变化会会导致全局问题。 3)做出细粒度决策来适应这种变化,这些决策很多来自于直觉。 4)通过实践来平衡对灵活性的要求。 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 现在我们已经知道,我们可以很容易得把架构设计的很灵活,但合理的架构是基于共识 和平衡设计的。过于灵活的设计存在着一些潜在的不良后果: 1)效率低 高度灵活的设计在一个接口两边都需要额外的运行处理,特别是使用了反射这样的技术 尤其如此,动态装入、动态参数很容易在接口操作上出现两个数量级以上的延迟。使用分布 式体系或者 SOA 架构都可能造成低效率的后果。如果架构强调了质量标准,这种低效率的不 可容忍可能会迫使你放弃灵活性。 2)可理解性差 如果架构太灵活,往往使开发人员难以理解你的架构,结果造成开发的困境。最坏的情 况是开发人员自己做了某些假定,是最后的结果和你的设想大相庭径。所以架构设计中的灵 活性应该在开发人员能够理解的前提下实现,而且需要和开发人员加强沟通。 3)冗余编码 灵活性设计必然导致冗余编码,这样的冗余带来的好处是在变化的过程中不需要修改架 构。但是代价是编码量确实提高了,这就提高了开发成本,这是需要做出某种平衡的。 4)过多的文档化约定 灵活性设计往往需要配置文件,或者是用文档来规定约束,这种应用上的复杂性如果不 加以控制,这就会使软件集成的难度增加。在硬编码和使用约定之间的权衡,需要一个准确 的设计平衡点。 3,水平设计元素 尽管垂直设计非常受程序人员欢迎而且高效方便,架构师还是应该把眼光落在水平设计 元素上。在需求工程完成以后,架构的设计基本上是属于垂直方法,也就是根据需求来构架 体系,接着,合理的消除垂直设计元素成为一个架构师能力的体现。 关键是合理。 需求经过领域分析以后,软件设计就开始了。领域分析可以在一个给定的问题域里面, 帮助架构师确定水平元素和垂直元素。就设计方法而言,由分析人员那里得到的知识和经验 尤为重要。 一个好的水平设计通常能满足多个应用要求,不过过分的通用性一般也是很难达到的, 这就需要一个平衡。事实上一般化的问题往往比特定问题更容易解决,这是因为特定问题往 往把人的思维局限于具体细节中,而一般问题可以从具体的细节中解脱出来,但是复用仍然 是对一个架构师水平的考验。 7.12.3 职责驱动的开发 在企业应用架构设计过程中,我们要通过特殊的形式的讨论职责驱动的设计和开发。职 责驱动的开发是指根据子系统或者构件所应该具有的功能上的职责,对其进行分析和设计。 在一个系统中,子系统或者构件的职责集相互正交。如果新设计的子系统要承担的职责 已经存在了,就可以把这个职责委派给已经拥有这个职责的子系统、构件或者实例。这种技 术以非常小的委派的代价,最大限度地增加了复用。 这个过程的结果之一,就是为子系统创建软件规范。它以独特的格式区别了接口文档和 实现文档。因为接口要被其它子系统所使用,所以相较于封装在子系统中的类而言,要求更 加不容易变动,这就是一个十分重要的设计原则,接口要保持稳定,高层架构设计的时候, 也需要下很大的工夫规范接口。 在软件开发的时候,如果开发人员不能时常碰头,或者极端的某个子系统是由外包的形 xiexh@tianbo.com.cn ◆中科院计算所培训中心 高级软件系统架构师培训 式开发的,设计文档就更应该类似于设计规范,它比一般的描述方式要求更加严密。设计规 范应该更清楚地将子系统、构件间的接口,与详细描述构成系统的子系统内部实现部分加以 区分,其目的是尽可能的减少设计的二义性。 尽管如果类设计中方法、参数及数据结构足够详细,本来是不需要设计规范的。但实际 上设计是在不断改进,利用软件规范明确标定接口,就可以防止设计改动的时候带来问题。 一旦拥有了设计规范,架构时就可以和开发团队交流思想,通过“介绍”迅速沟通,必 要的时候也可以单独指导。在这样的“介绍”中所取得的反馈,也可以用来改进自己的架构, 甚至可以委托开发小组进行小粒度设计和实验,当然这些设计和实验必须符合设计规范,而 且是在构架师指导下进行的。 7.12.4 架构的可追踪性 一般来说,垂直设计的可追踪性是比较好的,因为它的实现细节欲外部需求是紧密联系 在一起的。但是当一个架构被设计成水平结构的时候,可追踪性就变得不明显了。解决的办 法是通过内部场景显示内部设计是如何支持外部需求的,每个场景都对应于一个重要的外部 功能的执行。典型情况下一个水平设计元素与多个外部场景有关,而不仅仅描述单个需求。 xiexh@tianbo.com.cn

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档

相关文档