软件设计原则、模式与应用

1,571 views

Published on

0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,571
On SlideShare
0
From Embeds
0
Number of Embeds
16
Actions
Shares
0
Downloads
34
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

软件设计原则、模式与应用

  1. 1. 软件设计原则、模式与应用 ( Software Design Principle,Pattern and Application ) 2005年10月
  2. 2. 一 、引言 1、在敏捷开发中,所涉及的原则分为: (1)开发原则 共12条。这些原则是从“实践”的角度给出的。 用于项目的组织 (2)设计原则 共11条。这些原则是从设计的角度给出的原则。 用于开发人员的行为指导。 其中• 为了解决所谓的软件“腐化”问题,提出了5条。 • 为包的设计(集成技术),给出了6条。
  3. 3. 2、原则提出的基本思想源 软件之美,在于: 1)、它的功能 对于用户来说,通过直观、简单的界面呈现出恰当特征的 程序就是美的; 2)、它的内部结构 对于软件设计者来说,通过简单、直观的划分,使其具有 最小耦合的内部结构就是美的; 3)、团队创造它的过程 对于开发人员和管理者来说,每周都会取得重大进展,并 生产出无缺陷代码的具有活力的团队过程就是美的;
  4. 4. 实现软件之美的途径--敏捷开发 所谓敏捷开发,就是一种面临迅速变化的需求快速开发 软件的能力。 获取这一能力,需要: 1)使用一些实践,以提供必要的纪律和反馈; 2)使用一些设计原则,以保持软件是灵活的、可维护的; 3)掌握一些设计模式,以便针对特定问题权衡这些原则。
  5. 5. 3、敏捷开发(主要包括敏捷实践和敏捷设计) 敏捷联盟的宣言,具体表达了敏捷开发的基本思想。 敏捷联盟宣言 我们正在通过亲身实践,并通过帮助他人的实践,揭示更好的软件 开发方法。这些工作使我们认识到: • 个体和交互 胜过 过程和工具 • 可以工作的软件 胜过 面面俱到的文档 • 客户合作 胜过 合同谈判 • 响应变化 胜过 遵循计划 虽然右边各项也有价值,但左边的各项具有更大的价值。 Kent Beck James Grenning Robert Martin Mike Beedle Jim Highsmith Steve Mellor Arie van Bennekun Andrew Hunt Ken Schwaber Alistair Cockburn Ron Jeffries Jeff Sutherland Ward Cunningham Jon Kern Dave Thomas Martin Fowler Brian Marick
  6. 6. 二、设计原则 1、引言 1)、基本思想 • 敏捷设计是一个过程,而不是一个事件。 • 该过程是一个不断应用原则、模式和实践,来改进 软件结构 和 可读性的过程。 • 其目标是,保持系统设计在任何时间都尽可能的简单、 干净(主要是指边界清楚,结构良好)和富有表现力。
  7. 7. 2) 敏捷开发思想的来源: “ 满足工程设计标准的唯一文档是源代码清单”。 -Jack Reeves 1992 ,“C++ Journal” 在这篇文章中, Jack Reeves对C++的流行,思考了一个问 题,即什么是真正软件设计。按着他的观点: (1)“ 满足工程设计标准的唯一文档是源代码清单”。 他通过仔细审视软件开发的生命周期,编译器和连接器 是实现软件构建的基本工具。其廉价程度是令人难以置信 的。并且,随着计算机速度的加快,软件构建将变得更加 廉价。 —这是软件业和传统制造业之间的一个重要区别! (设计团队—设计文档;制造团队——构建产品。 设计与制造独立;人员技能不同。)
  8. 8. (2)“设计实际软件的昂贵程度 是令人难以置信的, 之所以 如此,是因为软件是非常复杂的,并且软件项目的几乎 所有步骤 都是设计的一部分”。 A、 编程是一种设计活动,并且在编码显得有意义时,就 应立即进行编码。 其中: 可以使用一些支持高层设计的结构图表、类图、状态图 和PDL等工具和表示法,提供必要的帮助,但它们都不 是真正意义上的软件设计(不符合严格的工程设计标准), 因此必须创建真正的软件设计,并使用某种编程语言来 完成 之。
  9. 9. 软件开发过程模型的不断演化,可以表明,支持在软件 生存周期的更早时期开始编码,以便使得验证和测试更 早地对设计进行验证和改进。并且,还意味着高层设计 人员很有可能参与详细设计。 编码比我们所说的一般设计更具有表现设计意义。因为 其中会揭示出一些疏漏和额外的设计需求。因此编码发 生的越早,其设计就越好。
  10. 10. B、测试/调试是一种设计活动,就软件开发来说,它们相 当于其它工程学科中的验证和过程改进。 C、其他设计活动,包括高层设计、模块设计、体系结构 设计等。其中,所使用的许多不同的软件设计表示法, 它们不是软件设计,但可以作为辅助文档和工具,来 简化设计过程。具体地说,辅助文档的主要用途为: 从问题空间中捕获并记录一些在设计中不能直接使用的 重要信息。(这就是我们所说的需求规约) 软件设计要创建一些软件概念来对问题空间中的概念进行建模, 该过程需要得到一个对问题空间的理解。通常,这一理解包含 一些最后还不会直接建模到软件空间中的信息。但它们却有助于 设计人员确定什么是本质的概念,如何对它们给出很好的模型, 以便以后对模型的更改。
  11. 11. 记录设计中某些方面的重要内容。而这些内容是很难作为 注释包含在代码中。这些内容可以是高层的,也可以是低 层的。显然,图形是描述它们的最好方式。 体系结构文档是其中最重要的一个辅助文档。 • 一个好的 OO体系结构通常可以使用几幅图和十几页文字 描述出来。 • 但必须集中于设计中的关键类和关系。 • 必须由人来编写这样的文档和维护之。 • 最好在源代码完成后,或在编写源代码的同时编制这样 的文档,而不是在源代码编写之前进行。
  12. 12. 3)目前软件开发仍然还是一门工艺,而不是一门工程学科。 主要 因为在验证和改善设计的关键过程中,缺乏所需要的 严格 性。 4)软件开发的真正进步依赖于编程的进步,这意味着依赖于 编程语言的进步。回到本文开始提出的思考,可以回答为什 么C++最为流行,其根本原因是,它是一种直接支持软件设 计的主流编程语言。
  13. 13. 3)、拙劣设计的表征 (1)僵化性(Rigidity):是指难于对软件设计进行改动, 即使是简单的改动。 表现:如果一个改动会导致有依赖关系的连锁改动,那么 设计就是僵化的。必须改动的模块越多,其设计就 越僵化。 (2)脆弱性(Fragility):是指在进行一个改动时,程序的 许多地方就可能出现问题,即设计易于遭受破坏。 并且,往往是出现新问题的地方与改动的地方并 没有概念上的关联。
  14. 14. (3)粘固性(Immobility):是指设计中包含了对其它系统有 用的部分,但要想把这些部分分离出来所需要的努力 和风险是巨大的,即设计难于复用。 (4)粘滞性(Viscosity):具有以下2种表现形式: 软件粘滞性:是指当面临一个改动时,那些可以保持系 统设计的方法比那些破坏设计的“生硬”方法更难应用, 即难于做正确的事情。 环境粘滞性是指环境的迟钝和低效。例如编译时间长。- 也是难于做正确的事情。
  15. 15. (5)不必要的复杂性(Needless Complexity):是指设计中包 含了当前没有用的成分,即过分设计。 危害:这种设计,一方面使软件变得复杂,另一方面使软件 难于理解。 (6)不必要的重复( Needless Repetition):是指滥用“剪切” 和 “粘贴”鼠标操作。 “剪切”和“粘贴”操作也许是有用的 文本编辑操作,但却是灾难性的代码编辑操作: 往往是开发人员忽略了抽象。从而使系统不易理解; 软件中的重复代码,使系统的改动变得更加困难, 不易系统的维护。
  16. 16. (7)晦涩性(Opacity):是指模块难于理解。代码随时间而 不断演化,往往会变得越来越晦涩。 办法:为了减少代码的晦涩性,需要持续地保持代码清晰 和富有表现力。
  17. 17. 引起软件腐化的原因: (1)需求没有按照初始设计所预见的方式进行变化,从而 导致设计的退化; (但不能因为退化而责备需求的变化) (2)改动的要求是紧迫的,并且开发人员对于原来的设计 思路并不熟悉,虽然可以进行一些改动,但却以某种方式 违反了原来的设计,并随着时间的推移,这些违反不断积 累,就使软件出现“臭味”。
  18. 18. 4)、防止软件腐化的基本途径 —依赖变化来获取活力 (1)团队几乎不进行预先(up-front)设计,因此不需要一 个成熟的初始设计;-“自低向上”设计 (2)团队使用许多单元测试和验收测试,支持系统的设计 尽可能的干净、简单,使设计保持灵活性和易于理解性; -测试驱动的设计 (3)灵活、持续地改进设计,以便使每次迭代结束时所生成 的系统具有满足那次迭代需求的设计。 -不断地实施重构
  19. 19. 2、设计原则 ( 为了指导以上途径的实践,给出了共11条设计行为准则。 其中,为解决以上诸多病症,给出了5条;对包的设计,给出 了6条。这些原则 支持开发人员的设计) • 单一责任原则 • 开放-封闭原则 • Liskov替换原则 • 依赖倒置原则 • 接口隔离原则 注:-软件工程一些原则的特例; -无条件地遵循这些原则是错误的做法; -过分地遵循这些原则将导致不必要的复杂性。
  20. 20. (一)单一职责原则(SRP) 只有佛自己应当承担起公布玄妙秘密的职责。 -E.Cobham Brewer,1810-1897 英语典故字典,1898 (1)、内容: 就一个类而言,应该仅有一个引起 它变化的原因。 这条原则被称为内聚性原则。 一般地,内聚性是指一个模块组成元素之间的功能相关性。 在本条原则中,把内聚性和引起一个模块或类的改变的 作用联系起来。
  21. 21. 为了理解这一原则,首先要回答什么是职责? 在SRP中,把职责定义为:引起变化的理由 (a reason of change)。 根据这一定义,显然可知:如果能够想到多个(>1)动机 来改变一个类,那么这个类就具有多于一个的职责。 实践中,人们很习惯以“组”的形式来考虑类的职责。例如: 一个接口“调制解调器”有四个功能: interface modem { public void dial (string pno); public void hangup ( ); public void send (char c); public void recv ( ); }
  22. 22. 根据职责的定义,可以认为该接口有两个职责: 连接处理(public void dial (string pno); public void hangup ( );) 数据通信(public void send (char c); public void recv ( );) 这一接口“调制解调器”显然违背了SRP原则! (2)问题:是否将这2个职责分离? 这取决于应用程序变化的方式: 如果应用程序的变化会影响连接函数的声明(signature), 那么调用send和recv就必须重新编译,因此应分离这两种职责。 如果应用程序的变化总是导致这两个职责同时变化,那么 就不必分离这两种职责。
  23. 23. (3)、如何实现 耦合职责的分离? 就上一个例子而言,可以把2个职责分离为: 《interface》 《interface》 Data Channal Connection +send(:char) +dial(pno:String) +recv( ):char +hangup( ) Modem Implementation 其中,可以把ModemImpiementation类看作是一个杂凑物 (kludge),通过分离它们的接口,解除耦合的概念(“连接” “通信”)。这样,使所有对该接口的依赖,都与 ModemImpiementation类无关。除了main外,谁也不知道它 的存在。
  24. 24. (4)、一种常见的违反该原则的情况—持久化问题 如左图所示: 其中,类Employee包含了业务 Employee Persistence 规则和持久性控制。这2个职责 +CalculatePay Subsystem +Store 在多数情况下不应该合在一起。 因为业务规则往往会不断的变化, 并且变化的原因也各不相同。 (5)如何处理职责耦合: 第一,测试驱动的开发实践,往往可以避免这种情况发生, 即这一实践会迫使对类的多种职责进行分离。-主动式 第二,如果出现了这种情况,可以使用FAÇADE和 PROXY 模式对设计进行重构,分离多种职责。 -被动式
  25. 25. 附0:测试驱动的软件开发 如何看待单元测试? 单元测试是 一种验证行为, 更是一种设计行为。同样 它更是一种编写文档的行为。 编写单元测试,可以避免相当数量的反馈循环,尤其是功能 验证方面的反馈循环。
  26. 26. 1) 思考与问题: 如果在设计程序之前先设计测试方案,情况将会怎样? 如果能够做到除非缺少某一功能而导致测试失败,否则就 拒绝在程序中实现该功能,情况将会怎样? 如果能够做到除非缺少某行代码而导致测试失败,否则就 拒绝在程序中增加哪怕一行的代码,情况又会怎样? 如果首先编写失败的测试,表明需要一项功能,然后增加 这一功能使测试通过,情况将会怎样? 以上的思考,将对编写软件的设计产生什么影响? 如果存在以上问题所涉及的测试,我们能够得到什么好处?
  27. 27. 2) 测试先于设计的示例 一个简单的游戏程序,名为-“Hunt the Wumpus”。 该游戏的内容为: 玩家(WumpusGame)在洞穴中移动,在没有被Wumpus吃掉 前,设法杀死它。 其中:1)该洞穴由一系列通过过道相连的房间组成; 2)每个房间都有向东、向西、向南向北的通道; 3)玩家通过告诉计算机要行进的方向而四处移动,以便 实现其自己的目标-设法杀死Wumpus。
  28. 28. 在编写WumpusGame之前,首先编写了一个测试程序testMove : public void testMove() { WumpusGame g=new WumpusGame() ; g.connect(4,5,”E”); g.setPlayerRoom(4); g.east(); assertEquals(5, g.getPlayerRoom( )); } 注:在编写WumpusGame之前,先在其测试中陈述你的意图。这种方法一般 称为“有意图的编程(intentional programming)”。可以简单、清晰地给程 序指出一个好的结构。
  29. 29. 通过这一测试可以看出: (1)没有引入“Room”类!仅使用整数表示房间。 作者认为:就游戏而言,连接(从一处到另一处)概念 比“房间”概念更重要。这就是一个意图。 这样,在早期阶段就阐明了一个设计决策。 (2)通过该测试,可以使编程人员了解该游戏程序是如何工 作的。 (3)根据这一简单的规格说明, 可以很容易地实现WumpusGame中的已经命名的方法, 可以很容易命名其它3个方向并实现之。
  30. 30. 3)测试可以促使模块之间的分离 例如,一个薪水支付应用,在没有编码前,简单、快速地划了一个UML 图: CheckWriter Employee Payroll +writerCheck( ) +caculatePay( ) +postPayment( ) Employee Database +getEmployee( ) 其中, +putEmployee( ) 类Payroll使用EmployeeDatabase获得Employee对象,并要求 Employee来计算自己的薪水。 接着,它把计算结果传给CheckWriter对象,产生一张支票。 最后,它在Employee对象中记录支付信息,并把Employee 对象写回到数据库中。
  31. 31. 现在,要编写 规定Payroll对象行为的测试。其中需要 考虑的问题有: 使用什么样的数据库? Payroll对象需要从哪些数据库中读取数据? 在测试前,需要把什么数据加载到数据库中? 如何检验打印出来的支票的正确性? …… 对于以上这些问题,可以使用MOCK OBJECT模式解决之。即 在Payroll类和它的所有协作者之间插入接口,创建实现这些 接口的测试桩。如下图所示:
  32. 32. Mock Mock CheckWriter PayrollTest Employee 《interface》 《interface》 CheckWriter Payroll Employee +writerCheck( ) 《interface》 +caculatePay( ) +postPayment( ) Employee Database +getEmployee( ) +putEmployee( ) Mock Employee Database
  33. 33. 从上图可以看出, 现在Payroll类使用接口和CheckWriter、 Employee 、 EmployeeDatabase 进行通讯,创建了实现这些接口的Mock Objects。 PayrollTest对象对这些Mock Objects进行查询,以检验 Payroll对象是否正确地对它们进行了管理。
  34. 34. 这一测试的设计思想是: 创建适当的Mock Objects,并把它们传递给Payroll对象, 告诉Payroll对象为所有雇员支付薪水,接着要求Mock Objects 去检查所有已开支票的正确性以及所有已支付信息的正确性。 显然,这一测试所检查的都是Payroll应该使用正确的数据调 用正确的函数。它既没有真正地去检查支票的打印,也没有真 正地去检查真实数据库的正确刷新。 相反,它检查了Payroll类应该具有与它独立情况下同样的行 为。其中,不引入MockEmployee类,而是直接使用Employee类, 这是可以的,但使 Employee类显得复复杂了一些。
  35. 35. 程序: Public void testpayroll() { MockEmployeeDatabase db=new MockEmployeeDatabase(); MockCheckWriter w=new MockCheckWriter(); Payroll p= new Payroll(db,w); p.payEmployees(); assert(w.checksWereWritenCorrectly(); assert(db.paymentWerePostedCorrectly(); }
  36. 36. 在这一测试中,引入了Payroll类,这不但是为了测试,也是 给出了一个设计决策!实现了“处理”与环境(数据库,打印 机)的分离。这样就可以互换使用不同类型的数据库和打印 机,以便应用的扩展。
  37. 37. 对开始问题的回答: 第一个影响是,程序中的每一项功能都有测试来验证其操 作的正确性。这一套测试程序可以为以后的开发提供支持--可 以对程序功能和结构的改变提供支持。 一个重要的影响是,首先编写测试,可以迫使我们使用不同 的观察点。其中,必须从调用的角度,来观察所要编写的程序 。即在关注功能的同时,直接关注它的接口。编写一个便于调 用的软件。 首先编写测试,可以迫使我们把程序设计为可测试的。 注:为了实现一个便于调用的、可测试的程序,就必须“简 化”该程序与其环境的耦合。-实现结构美。 首先编写测试的一个重要效果是,测试可以作为一种无价的 文档形式-是可编译的、可运行的。这样的测试可以帮助其他程 序员如何使用代码。
  38. 38. 结论 (1)单元测试和验收测试都是一种文档形式-是可编译的、 可 执行的;因此它是准确和可靠的。 (2)编写测试的语言是明确的,即程序员能够阅读单元测试, 客户可以阅读验收测试。 (3)测试套件越简单,就会频繁地运行之。测试运行得越多, 就会越快地发现那些与测试不符的问题。 (4)测试最重要的好处是对体系结构的影响。因为对于一个 模 块或一个应用而言,越是具有可测性,其耦合关系就 越弱。
  39. 39. 附1: FACADE模式 (1)该模式解决的问题:职责耦合 (2)应对的情况: 当要为一组复杂而全面的接口的对象(例如Java.sql包) 提供一个简单且特定的接口时,可以使用这一模式. (3)基本途径(目标): 把某些策略施加到另外一组对象上. 从而实现了职责的 分离 [ 注:FACADE模式从上面施加策略, 因而使用这一模式是要 受 到限制的。]
  40. 40. (4)该模式所具有的结构:例如 DB ProductData +store(ProductData) Application +getProductData(sku) +deleteProductData (sku) Java.sql Connection Statememt Driver Manager Prepared ResultSet Statememt SQLException
  41. 41. 其中, DB是一个FAÇADE类(呈表类),特定于 productData, 提供了一个非常简单 的 接口。Application不必了解Java.sql 的细节,把Java.sql包所有的复杂性隐藏在一个非常简单的 接口中. 对Java.sql包的使用,DB类向该包施加了许多策略.例如 如 何初始化和关闭数据库连接,如何将ProductData的成员 变量转换为数据库字段,如何构造合适的查询和命令来 操纵数据库. 使用FAÇADE模式,意味着开发人员接受了有关DB的 约定,即用户(Application)的任意部分的代码都必须通过 这样的DB来访问Java.sql包-受限的方式,不能越过 FAÇADE类而直接访问Java.sql。 注意:基于这一约定,使DB成为Java.sql包的唯一代理 (broker).
  42. 42. 附2:PROXY模式 (1)解决的问题:职责分离(分离“环境”的职责) (2)应对的情况 软件开发中存在许多障碍,例如 当把数据移到数据库中时,要跨越数据库障碍; 当把消息从一台计算机发送到另一台计算机时, 要跨越网络障碍。 通过PROXY模式,既可跨越这些可能复杂的障碍, 又能保持关注本身要解决的问题。
  43. 43. 3)PROXY模式结构及工作原理 如下所示: (以购物车系统为例) 《interface 1 Proxy模式的静态结构 》 Product 3 2 Product 《delegates Product DB DB Proxy 》 Implementation 每个要被代理的对象(“产品”)被分为3个部分: 第一部分是一个接口,它声明了客户端要调用的所有方法; 第二部分是一个类,该类在不涉及数据库逻辑的情况下实 现了该接口中的方法; 第三部分是一个知晓数据库的代理。
  44. 44. 其中 (1)对于product类,通过用一个接口实现了对它的代理。如 上所述,该接口有product类的所有方法,这些方法从数据 库中取出产品。为此,它创建一个ProductImplementation 实例,然后再把消息委托给这个实例。 下面Proxy模式的动态结构展示了该模式是如何工作的: Product DB DB Proxy getPrice() retrieveProduct(sku) Product price Implementation product getPrice() price Proxy模式的动态结构
  45. 45. 工作过程: 客户向一个它认为是product、但实际上是productDBProxy 的对象发送getPrice消息。 ProductDBProxy 从数据库中创建ProductImplementation, 然后把getPrice方法委托给它。 (2)Proxy模式的优点:客户和ProductImplementation都不知 道所发生的事情。数据库在这两者都不知道的情况下被插 入到应用程序中。因此,该模式的最大好处是,可以实现 一些重要关系的分离(separation of concerns)。 就这一例子而言,实现了业务规则和数据库的分离。这是 当前最流行的一种保持业务规则和实现机制分离的方法。
  46. 46. 关于单一职责原则的总结 1、单一职责原则(SRP)是最简单的一个原则,但也是最难 运用的原则之一; 2、软件设计指导之一: 在软件设计的整个过程中,一个主要工作,就是不断地发 现职责,并把这些职责进行分离。(本质上,其它原则都是以 这种方式或那种方式回答如何发 现职责,并进行分离。) 3、在设计实践中产生违背这一原则的基本原因: 人们会自然地把职责合在一起。 4、主动地或被动地解决职责耦合问题的途径 实践中,采取以测试驱动的软件开发; 采用有关的设计模式。
  47. 47. (二)开放-封闭原则(The Open-Closed Principle,OCP) 1)、内容:软件实体(类、模块、函数等)应该是可扩展 的,但是不可修改的。 (1)“对于扩展是开放的”(open for extention) 这意味着模块的行为是可以扩展的。换言之,可以改变模 块的功能。 (2)“对于改变是封闭的”(closed for modification) 这意味着对模块行为改变时,不必改动模块的源代码或 二进制代码。模块的二进制可执行版本,无论是可链接的 库、DLL或Java的.jar文件,都无需改动。
  48. 48. 2)、实现这一原则的基本思想、机制和结构 (1)基本思想: 关键的是抽象! (2)机制:继承 不允许修改的模块,通常被认为是具有“固定”行为的模 块。抽象基类以及可能的派生类,就能够描述一组任意 可能行为的抽象体。 应用模块可以操作一个抽象体。由于该模块依赖一个“固 定”的抽象体,因此 对于更改来说,可以认为该抽象体是封闭的; 但通过该抽象体的派生,可以扩展该模块的行为, 这样可以认为该抽象体是开放的。
  49. 49. ( 3 ) 实现该原则(OCP)的结构 例如: Client Server 由于, Client 类和Server类都是具体类, Client类是既不开放的 又不封闭的类 通过委托方式,实现开放-封闭原则(OCP)的结构: 《interface》 Client Client Interface 该接口体现了抽象! Server STRATEGY模式:既开放又封闭的Client
  50. 50. 其中, ClientInterface类是一个拥有抽象成员的抽象类。 Client 类使用这个抽象类,但Client 类的对象却使用Server类的派 生类的对象。 (1)封闭性的体现:如果希望Client 对象使用一个不同的 服务器类,那么只需从ClientInterface类派生一个新的类, 而无需对Client 类进行任何改动。 (2)开放性的体现:如果Client 类需要实现一些功能,可 以使用ClientInterface的抽象接口来描述这些功能。 ClientInterface的子类型可以以任何方式来实现这个接口。 这样,就可以通过创建ClientInterface的新的子类型的方式 来扩展、更改Client 中指定的行为。
  51. 51. 附3: STRATEGY模式 1) 所要解决的问题: STRATEGY模式可以用来 分离通用算法,并允许高层 算法独立于它的具体实现得以复用. 2) 途径: STRATEGY模式是使用委托来解决问题,并且 STRATEGY模式还允许具体实现细节独立于高层的算法 而得以复用。(不过需要付出一些额外的复杂性、内存和运行时 间为代价.)
  52. 52. 3) 基本结构 为了使用委托来实现通用算法与具体实现的分离,该模式倒置 了通用算法和具体实现之间的依赖关系。例如: 《interface》 Application Application +init Runner +idle +run +cleanup +done:boolean ftocStrategy 具体地说: (1)不是把通用算法(+run)放在一个抽象基类中,而是放在名为 ApplicationRunner的具体类中。 (2)把通用算法必须要调用的抽象方法(例如: +init ,+idle等 )定义在名为 Applicatinn的接口中。 (3)从这个接口派生出 ftocStrategy,并把它传给ApplicationRunner。 之后, ApplicationRunner就可把具体工作委托给该接口来完成。
  53. 53. 该例的程序可以是: ApplicationRunner.java Public class ApplicationRunner { private Application itsApplication=null; public ApplicationRunner(Application app) { itsApplication=app;} Public void run() 通用算法(+run) { itsApplication.init(); While(!itsApplication.done()) itsApplication.idle() itsApplication.cleanup } }
  54. 54. Application.java Public interface Application { public void init(); public void idle(); 通用算法必须要调用的抽象方法 public void cleanup(); public boolean done(); }
  55. 55. ftocstrategy.java import java.io.*; Public class ftocstrategy implements Application { 从接口Application派生出 ftocStrategy, private InputStreamReader isr; private BufferedReader br; private boolean isDone=false; public static void main(Sring[] args) throws Exception {(new ApplicationRunner(new ftosStrategy())).run();} 把ftocStrategy传给ApplicationRunner。 public void init() { isr=new InputStreamReader(System.in); br=new BufferedReader(isr); } Init的实现
  56. 56. public void idle() Idle的实现 { Sring fahrString=readLineAndReturnNullIfError(); if ( fahrString==null fahrString.length()=0) isDone=thue; else { double fahr=Double.parseDouble(fahrString); double celcius=5.0/9.0*(fahr-32); System.out.println(“F=”+fahr+”, c=”+celcius); } } public void cleanup() Cleanup的实现 { System.out.println( “ftoc exit”); } public boolean done() { return isDone; }
  57. 57. private String readLineAndReturnNullIfError() { String s; try { s=br.readLine(); } catch(IOException e) { s=null;} return s; } } 综上,ApplicationRunner就可把具体工作委托给该接口来完 成。
  58. 58. 通过抽象基类的方式,实现OCP的另一结 构: Policy +PolicyFunction() -ServiceFunction() Implementation -ServiceFunction() Template Method模式:既开放又封闭的基类 其中,Policy类具有一组实现了某种策略的共有函数。这些策略函数使用 一些抽象接口描述了一些要完成的功能。但在这个结构中,这些抽象接口 是Policy 类的一部分。(它们在C++中表现为纯虚函数,在Java中表现为 抽象方法。) Policy类中的这些函数在Policy的子类型中予以实现。这样,可以通过从 Policy类派生出新类的方式,对Policy中指定的行为进行扩展或更改。 (体现了开放性和封闭性!)
  59. 59. 附4: (1) TEMPLATE METHOD模式 该模式把所有通用代码放入一个抽象基类的实现方法中,而 将所有实现细节都交付给该基类的抽象方法。如下所示: 基类A 方法:F { 通用算法 } 方法1(抽象方法):{实现细节} 方法2(抽象方法):{实现细节} ……
  60. 60. (2)TEMPLATE METHOD模式和STRATEGY模式的比较 TEMPLATE METHOD模式和STRATEGY模式都可以 用来把一个功能的通用部分和实现细节清晰地分离开来。 两个模式是满足OCP原则最常用的方法。其途径是,遵循 倒置依赖原则(DIP),使通用算法不依赖具体的实现,并 使通用算法和具体实现都依赖抽象. 2种模式的区别:继承和委托 尽管TEMPLATE METHOD模式和STRATEGY模式所要解决的问题是 类似的,而且可以互换使用. 但 TEMPLATE METHOD模式使用继承来解决问题, 而STRATEGY模式是使用委托来解决问题.
  61. 61. 结论 1、在许多方面,开发-封闭原则(OCP)都是面向对象设计的 核心。遵循这一原则,可以带来很大的好处: -灵活性、可复用性以及可维护性。 Ivar Jacobson曾经说过:“任何系统在其生存周期中都会 发生变化。如果期望开发的系统不会在第1版本后就被抛弃, 必须牢牢记住这一点。” 2、不是使用了面向对象语言就是遵循了这一原则。 3、对应用程序中的每一部分肆意地使用抽象,同样不是一个 好的主意。正确的做法是,开发人员应该仅对呈现频繁变 化的那些部分做出抽象。其中,“拒绝不成熟的抽象与抽象 本身一样重要的”。
  62. 62. (三)Liskov替换原则( LSP) OCP所隐含的主要机制是抽象和多态(polymorphism)。 在C++和Java这类语言中,支持抽象和多态的关键机制是 继承(inheritance)。正是使用了继承,才可能创建实现其基 类中抽象方法中的派生类。 然而, • 是什么设计原则支配这种特殊的继承用法? • 最佳的继承层次的特征是什么? • 怎样的情况会使创建的类层次陷入不符合OCP? 以上这些问题正是LSP原则要回答的。
  63. 63. 1、Liskov替换原则的内容: 子类型必须能够替换掉它们的基类型。 这一原则是Barbara Liskov在1988年首先提出的。 “这里需要如下替换原则:若对每个类型S的对象o1, 都存在 一个类型T的对象o2, 使得在所有针对编写的程序P中,用o1,替 换o2后,程序P行为功能不变,则S是T的子类型。 ” 若违反这一原则,会出现什么后果?例如: 假定有一个函数f,它的参数指向某个基类B的“指针”或“引 用”。同样,假定有B的某个派生类D,如果把D的对象作为B 类型传递给f,就会导致f出现错误的行为。那么D就违反了 LSP。显然,D对于f来说是脆弱的。
  64. 64. 2、例子 违反 LSP,常常以违反OCP的方式使用运行时的类型辨别(RTTI)而 导致的。这种方式往往是显式地使用一个if 语句或if/else,来确定一个对象 的类型,以便选择该类型的正确行为。考虑以下程序: struct Point {double x,y;}; struct Shape { enum ShapeType { square,circle } itsType; Shape(ShapeType t):itsType{} }; struct Circle:public Shape { Circle():Shape(Circle) {}; void Draw() const; Point itsCenter; double itsRadius; };
  65. 65. struct Square:public Shape { Sqaure():Shape(Sqaure) {}; void Draw() const; Point itsTopLeft; double itsSide; }; void DrawShape(const Shape& s) 运行时的类型辨别 { if (s.itsType==Shape::square) static_cast<const Square&>(s).Draw(); else if (s.itsType==Shape::circle) static_cast<const Circle&>(s).Draw(); }
  66. 66. 结论:这样就违反了LSP,从而使DrawShape违反了OCP。 原因是:它必须知道Shape类所有的派生类,且每次创建 一个Shape的派生类都必须要更改它。这是因为: Square类和Circle类是Shape的派生类,具有函数Draw() 但没有重写(override) Shape 中的函数。由于Square类 和Circle类不能替换Shape,所以DrawShape函数必须检查输入 的Shape对象,确定它的类型,继之调用函数Draw()。 Square类和Circle类不能替换Shape,这样就违反了LSP,从而 使DrawShape违反了OCP。
  67. 67. 还有其它一些方式,可以引发违反Liskov替换原则。例如: • is A关系的使用 -没有把子类中那些不具有多态性的函数声明 为虚函数 • 基于契约设计的使用 -派生类违反了有关基类的约定 • 在派生类的方法中填加了其基类不能割舍的异常。
  68. 68. 结论: 1、LSP是导致违背OCP的主要原因之一。 正是子类型的可替换性,才使使用基类的模块在无须改 变的情况下可以予以扩展。 2、这种可替换性必须是开发人员可以隐式依赖的东西。因此 如果没有显示地强制基类类型的契约,那么代码就必须良 好地并显式地表达出这一点。 3、“is-A”的含义过于宽泛,以至于不能作为子类型的定义。 子类型的正确定义是“可替换性的”,这里的可替换性可以 通过显式或隐式的契约予以定义。
  69. 69. (四)依赖倒置原则( DIP) 1、内容: a. 高层模块不应该依赖低层模块。二者都应该依赖抽象。 b. 抽象不应该依赖细节。细节应该依赖抽象。 该原则是框架设计的核心原则 2、层次化 Booch曾经说过:“所有结构良好的面向对象构架都具有清 晰的层次定义,每个层次通过一个定义良好的、受控的接口向 外提供一组内聚的服务。”
  70. 70. 如果对以上这句话只是简单地予以理解,就有可能会出现以 下结构: Policy Layer Mechanism Layer Utility Layer 这一结构存在一个潜伏的错误特征: (1)Policy layer对于其下的任一改动,包括对 Utility Layer 的改动,都是敏感的。 (2)这种依赖是可传递的。 关于这一结构的评价:这一结构是不好的!
  71. 71. 就面向对象技术而言,层次化的合适模型应该是: Policy •每个较高层次为它所 《interface》 需要的服务声明一个抽 Policy Policy Service Layer 象接口,较低层次实现 Interface 这些抽象接口; •每个较高层次都通过 Mechanism 抽象接口使用下一层。 《interface》 Mechanism Mechanism Service 这样 Layer Interface 1)高层就不依赖低 层,而低层则依赖高 Utility 层; 2)不仅解除了其中的 Utility 传递依赖关系,而且还 Layer 解除了高层与其它层的 依赖关系。
  72. 72. 倒置的接口所有权问题 这就是著名的Hollywood 原则“Don’t call us, we’ll call you.” 即低层模块实现了在高层模块中声明并被高层模块调用的 接口。 依赖于抽象 这是解释DIP规则的一个启发式规则。该规则建议不应该依赖 具体类-即程序中的所有依赖关系都应该终止于抽象类或接口。 根据这个启发式规则,可知: • 任何变量都不应该持有一个指向具体类的指针或引用; • 任何类都不应该从具体类派生; • 任何方法都不应该覆写它的任何基类中已实现的方法。
  73. 73. 实践中几种具体情况的说明: 1)在编程中,有时必须要创建具体类的实例,而创建这些 实例的模块将会依赖它们。这说明程序中一般都会出现违反启 发式规则的情况。 2)如果一个具体类不太会改变,且不会创建其它类似的派生 类,那么依赖它也不会造成损害。这说明对那些稳定的具体类 而言,启发式规则似乎不大合理。 3)如果具体类是不稳定的,且还不想直接依赖之,则可以把 它们隐藏在抽象接口之后,以隔离它们的不稳定性。 4)依赖倒置规则可应用于任何存在一个类向另一个类发送消 息的地方。
  74. 74. 3、违反依赖倒置原则的例子: Lamp 这是一个以Button控制Lamp Button +poll() +turnOn() 的系统。其中,Button类直接 + turnOff() 依赖Lamp对象,这个设计不能 其对应的Java : 让Button控制其它设备。 public class Button 该设计方案违反了DIP,即应 { 用程序的高层策略没有与低层 provide Lamp itsLamp; 策略相分离,自然就使抽象依 赖于具体细节。 public void poll() 什么是高层策略?它是应用 { if (/* some condition */ ) 的抽象,是那些不随具体细节 itsLamp.turnOn();} 而改变的“真理”。它是系统内 部的系统-隐喻(metaphore). }
  75. 75. 通过倒置对Lamp的依赖关系,可以形成以下设计: 《interface》 Button ButtonServer +poll() +turnOn() + turnOff() Lamp 其中,由接口ButtonServer提供一些抽象方法,Button可以使用这些方 法对有关设备进行控制。由Lamp来实现 接口ButtonServer。 这样的设计就具有很好的灵活性。但问题是:Lamp不可能还受其他类的 控制。(原因:由于Lamp实现了接口ButtonServer.)
  76. 76. 结论: 1、使用传统的过程化设计所创建的依赖关系结构,策略是依 赖于细节的。这样会使策略受到细节改变的影响。 2、面向对象程序设计倒置了依赖关系的结构,是策略和细节 都依赖抽象,并且通常是客户的服务接口。这种依赖关系的倒 置,正好是面向对象的标志所在。 3、依赖倒置原则是实现许多面向对象技术所宣称的那些益处 的低层机制。它的正确应用对创建可复用的框架是必须的。同 时对建造具有弹性的代码(应对变化)是非常重要的。 4、应用该原则可以做到抽象和细节彼此分离,因此代码也易 于维护。
  77. 77. (五)接口隔离原则( ISP ) 1、内容: 不应强迫客户(client)依赖它们不用的方法。 解决的问题:ISP原则用来处理“胖接口”所带来的缺点。 何谓胖接口?如果类的接口不是内聚的,即接口可以分解 为多组方法,每一组方法服务于一组不同客户程序,则称 该接口是胖接口。 2、胖接口所到来的问题--接口污染 考虑一个安全系统。其中有一些Door对象,可以加锁和解 锁,并且知道自己所处的状态(开/关)。
  78. 78. class Door { public: virtual void Lock()=0; virtual void UnLock()=0; virtual bool IsDoorOpen()=0; } 显然,这是一个抽象类。客户程序可以使用符合Door的那些接 口对象,而不依赖Door的特定实现。 现在考虑这样一个实现-TimedDoor.其中,如果门开着的时间 过长,就会发出警报。为此, TimedDoor对象需要和一个名为 Timer的对象进行交互。即如果希望得到超时时间,就可以调用 Timer的Register函数,该函数有2个参数:一个是超时时间,另 一个是TimerClient对象的指针,该对象的TimeOut函数会在达 到超时时予以调用。
  79. 79. 现在的问题是,如何建立类TimerClient和类TimedDoor之间 的关联,才能在超时时通知TimedDoor中相应的处理代码? 下面给出了一种可想到的方案: Timer 《Interface》 0..* Timer Client +Timeout 其中,Door继承了Timer Client, 因此TimedDoor也继承了Timer Door Client, 这就可以保证TimerClient把自己注 Timed Door 册到Timer中,并可接收TimeOut的消 息。
  80. 80. 现在,类Door依赖于TimerClient。!该方案的主要问题就出现 在这里。 1)不是所有种类的Door都需要定时功能。最初的Door与定 时功能没有任何关系,如果需要创建一个没有定时功能的派生 类,那么就必须要提供TimeOut方法的“退化”实现,这就违反 了接口分离原则。 2)另外,使用这些派生类的应用程序,即使不使用定义的 TimerClient,也必须引入之,因此就具有不必要的复杂性和不 必要的重复-“臭味”。
  81. 81. 这是一个接口污染问题: 1)Door的接口被一个它不需要的方法污染了-在Door的接 口中加入这个方法只是为了给它的子类带来好处。 2)如果这样持续下去的话,每一次子类需要一个新方法 时, 就被加入到基类中,这样就可能进一步污染了接口,使它 “胖” 了起来。
  82. 82. 3、实现分离接口的途径 1)分离客户(程序)就是分离接口 Door的接口和TimerClient的接口完全被不同的客户程序所使 用,即Timer使用TimerClient,而对Door的操作使用了类Door。 既然客户程序是分离的,所以接口也应该保持分离。原因是 客户程序对它们使用的接口是有影响的。
  83. 83. 2)使用委托 分离接口 创建一个由TimerClient 所派生的对象,并把该对象的请求委 托给TimedDoor. 其中, 当TimedDoor Timer 《Interface》 想要向Timer对象注册 0..* Timer Client Door 一个超时请求时,它就 +Timeout 创建一个 DoorTimerAdapter,并 把它注册给Timer. Door Timer Adapter TimedDoor 当Timer对象发送 +Timeout() +DoorTimeOut Timeout消息给 DoorTimerAdapter时, 《creates》 DoorTimerAdapter把 这个消息委托给 TimedDoor.
  84. 84. 对使用委托 分离接口这一方案的分析: 1) 该方案遵循了接口分离原则(ISP),避免了Door的客户 程序之间的耦合. 2) 即使对Timer的进行了修改,也不会影响Door的使用者. 其中 一例中Timer的定义如下: Class Timer { public: void Register(int timeout,int timeoutID,TimeCclient* client); }; Class TimerClient { public: virtual void TimeOut( int timeoutID)=0; };
  85. 85. 3) Timed Door也不必具有和 TimerClient一样的接口; 4) DoorTimerAdapter会将TimerClient接口转换为TimedDoor 接口. 因此,这是一个非常通用的解决方案。 5) 该方案尽管是一种通用的,但不是很优雅的.即每次想去注 册一个超时请求时,都要创建一个新的对象.这对于那些对 内存和运行时间要求高的系统而言,例如嵌入式实时系统, 就显得不够理想。
  86. 86. 附5:Adapter模式 问题的提出:设计一个运行于台灯的软件 Light Switch +turnOn + turnOff 其中,Switch 对象不断地了解开关的状态,并可以向Light 发送一个相应的 turnOn 和 turnOff 消息。 这一设计违反了2个设计原则: 依赖倒置原则(DIP): Switch 依赖了具体类Light 。 开放封闭原则(OCP):在任何需要Switch 的地方都需要附带 Light,从而不易扩展Switch,使之去管理除Light之外的其它对象。
  87. 87. 为了解决以上问题,可以使用ABSTRACT SERVER模式和 ADAPTER模式 1)ABSTRACT SERVER模式 ABSTRACT SERVER模式,是在Switch和Light之间引入一个接 口,使Switch能够控制任何实现这个接口的设备。如下所示 : 《interface 》 Switch Switchable +turnOn + turnOff Light +turnOn +turnOff
  88. 88. 关于ABSTRACT SERVER模式的评注: (1)该设计的特点: 满足DIP和OCP。 (2)以往设计的一个误导: 将具有很强实体关系的继承层次结构放在一个包中。 使用ABSTRACT SERVER模式所引入的接口属于它的客户,而 不是Switch的派生类。 《interface 类A 》 Switchable 类A 类A 类A … +turnOn … … 类A + turnOff … 为了符合设计原则,应该把客户和他们所控制的接口打 在一个包中。(以后详谈之)
  89. 89. (3)产生这一误导的基本根源: 逻辑关系和实体关系强度是不一致性的。 这是静态类型语言(C++和Java)的产物;而动态类型 语言(Smalltalk、Python和Ruby)不具有这种不一 致性。因为它们没有用继承来实现多态性。
  90. 90. 2)ADAPTER模式 问题:以上的解决方案可能会违反单一职责原则(SRP)。 即如果从第三方购买了Light,而没有源代码怎么办?或如果想让 Switch控制其它一些类,但却不能由Switchable派生怎么办? 此时,可以使用ADAPTER模式. 由Switchable 派生出一个适 配器,并委托给Light。这样 Switch就可以控制任何可以 “ 打开”和“关闭”的对象。如下所示: 《interface Switch 》 Switchable +turnOn + turnOff Light Adapter Light 《delegates +turnOn +turnOn +turnOff 》 +turnOff
  91. 91. 其中: 1) Switch控制的对象可以不具有和Switchable 中一样的 turnOn和turnOff方法。适配器会适配到对象的接口。 2)LightAdapyer类被称为对象形式的适配器。还有一种称为类 形式的适配器,如下所示: 《interface Switch 》 Switchable Light +turnOn +turnOn 注:这种形式的适配器要比对 + turnOff +turnOff 象形式的适配器高效一些,且 容易使用,但付出的代价是: Light Adapter 使用了高耦合的继承关系。 +turnOn +turnOff
  92. 92. 3)使用多继承 分离接口 下图给出使用多继承并达到遵循ISP的方案: Timer 《Interface》 0..* Timer Client Door 其中,在这个模型中, TimedDoor同时继承 +Timeout 了Door和TimerClient. 这样,尽管这2个基类的 客户程序都可以使用 TimedDoor TimedDoor,但在实际 上却都不再依赖 +DoorTimeOut TimedDoor.从而,它们 就通过分离的接口使 比较: 就这一问题的2),3)方案而言, 用同一对象. 只有当Door Timer Adapter对象所做的转换是必须 的,或不同时候回需要不同转换时,才会选择2)中给出 的方案.
  93. 93. 结论 1、胖类可以导致它们的客户程序之间产生不正常的、且有 害的耦合关系。当一个客户程序要求该胖类进行一个修改时, 会影响到其他所有客户程序。因此,客户程序应该仅仅依赖它 们实际调用的方法。 2、把胖类的接口分解为多个特定于客户程序的接口,可以 实现以上目标。每个特定于客户程序的接口仅仅声明它的特定 客户或客户组调用的那些函数,接着该胖类就可以继承所有特 定于客户程序的接口,并实现它们。这就解除了客户程序和它 们没有调用的方法之间的依赖关系,并使客户程序之间互不依 赖。
  94. 94. 关于包的设计原则 问题的提出: 对一个小型应用来说,类是一种非常方便的组织单元。但对 于一个大型应用而言,其“粒度”就显得小了一些。为此,引入 “包”的概念,作为一种大粒度的组织单元。 关于包的设计原则: 围绕包的内聚和耦合,给出6个原则,以指导包的创建、相互 关系的管理和包的使用。其中: 前3个原则是关于包的内聚性的,关系到如何把类划分为包, 即包的“粒度”。 后3个原则是关于包的耦合性的,关系到如何处理包之间的相 互关系,即包的“稳定性”问题。
  95. 95. 第一条原则 复用发布等价原则(REP) 复用的粒度就是发布的粒度。 注: 1)如果一个包中的软件是用来复用的,那么就不能包含那些 不是为了复用而设计的软件。一个包中的软件,要么都是 可复用的,要么都不是可复用的。 2)考虑复用软件的人,希望一个包中的所有类,对于同一类 用户都是可复用的,不希望一个用户发现包中所包含的类 一些对他是所需要的,而另一些对他是不适合的。
  96. 96. 第2条原则 共同复用原则(CRP) 一个包中的所有类应该是共同复用的。如果复用了包中的一 个类,那么就要复用包中的所有类。 注: 原因:类很少会孤立的复用,一般需要与该类所描述的抽 象相关的一些类进行协作。-它们应该在同一包中。 结论:没有紧密联系的类,不应该在同一包中。
  97. 97. 第3条原则 共同封闭原则(CCP) 包中的所有类对于同一类性质的变化应该是共同封闭的。 一个变化若对一个包产生影响,则将对该包中的所有类产生 影响,而对其它包不产生任何影响。 注: 1)这是单一职责原则对于包的规定。这条原则规定了一个包 不应该包含多个引起变化的原因。 2)这条原则通过把对于一些被确定的变化种类开放的类共同 组织在同一包中,可以增强可维护性;可以增强进行有策 略的封闭,使所设计的系统对我们经历过的最常见的变化 做到封闭。
  98. 98. 以上3条原则所关注的内聚性,显然,比以往所说的模块的 内聚性具有更丰富的内容。 在组织包中的类时,要认识到可复用性和可开发性之间的相 反作用力,对它们要进行一定的权衡。其中,这种权衡是动态 的,即当项目的重心从开发性向可复用性转变时,包的组成可 能需要变动。
  99. 99. 以下3条原则是用来处理包之间关系。其中仍存在可开发性和逻辑设计之 间的相反作用力,或来自技术方面,或来自行政方面,都会影响包的组织。 第4条原则 无环依赖原则 在包的依赖关系图中,不允许存在环。 注: 1)原因:“晨后综合症”。 2)解决这一问题的途径是:-均来自电信业 (1)每周构建 •适用于中等规模的项目。 •每周5进行各自的更改,并构建系统。 问题是: • 随着项目的增长,集成工作变得无法在周末完成; • 随着开发和集成时间比率的降低,团队的效率随之降低。
  100. 100. (2)消除依赖环 为了解决上述的问题,可以把开发划分为多个可发布的包。 每个包可以作为一个工作单元由开发人员或一个开发团队予以 “检出”(check out).这样,所有开发团队都不会受其他团队的 支配。 其中,关键的问题是要对包的依赖关系结构进行管理。显 然,包的依赖关系结构中不能有环。若出现环的话,就不能避 免“晨后综合症”。
  101. 101. 下面给出一个包的无环依赖结构: MyApplication 1) 这是一个有向无环 图(DAG) Message Task MyTasks 2) 当负责MyDialogs的 团队发布一个新的 Window Window 版本时,按逆向便 Database 可知道,受影响的 包为MyTasks和 Tasks MyDialogs MyApplication。因 此负责这2个包的人 就要决定何时与 Window MyDialogs的新版本 进行集成。 3)当MyDialogs发布时,完全不会影响其它包。 4)当发布整个系统时,首先编译、测试和发布Window包,接之 是Message Window和Database,再后来是MyTasks,最后是 MyApplication。
  102. 102. 消除依赖环的方法: MyDialogs MyApplication 第一种方法:使用倒置依赖原则 如果出现以下情况: X Y 即新的需求迫使需要更改MyDialogs中的一个类X,使之使用 MyApplication中的一个类。这样就产生了2个包之间的一个依 赖关系环。 使用倒置依赖原则,可以创建一个具有MyDialogs需要的接 口的抽象基类,然后把该抽象基类放进MyDialogs中,并使 MyApplication中的类Y从其继承。这就倒置了它们之间的依赖 关系。如下所示 MyDialogs MyApplication 《interface》 X Y x Server
  103. 103. 第二种方法:新创建一个MyDialogs和MyApplication都依赖的包, 把MyDialogs和MyApplication都依赖的类移到该包中。如下所示: MyApplication Message Task MyTasks Window Window Database Tasks MyDialogs aNewPackage aNewPackage Window (1)这一方案,就需求的不断变更而言它是不稳定的。 (2)事实上,随着应用程序的增长,包的依赖关系结构会出现 抖动和增长,因此必须不断监控依赖环的问题。
  104. 104. 第5条原则 稳定依赖原则(SDP) 朝着稳定的目标进行依赖。 1)问题与解决 (1)要使设计是可维护的,某种程度的易变性是必要的。 达到这一目标,可以使用共同封闭原则(CCP),创建对 某些变化类型敏感的包,使这样的包是可变的。 但对于这样的包,就不应让一个难以改变的包依赖于它。 否则可变的包也难以改变。 (2)对于这样易于改变的包,其他人只要创建一个对它的 依赖,就可以使它变得难以更改,这就是软件的反常特性。 问题解决:遵循稳定依赖原则(SDP),可以确保那些打算 易于更改的模块,不会被那些比它们难以更改的 模块所依赖。
  105. 105. 稳定性 韦伯斯特认为:如果某物“不容易移动”,则认为它是稳定的。 例如:树立的硬币和放在地面上的桌子。 按着这一说法,稳定性与更改它所付出的工作量是有关的。 那么,就软件来讲,难以更改的因素很多,如规模、复杂性、 清晰程度等,但不考虑这些因素,使一个软件包难以“移动”的 可行方法是让许多软件包都依赖它。-往往需要大量的工作量 如下图所示: X包就是一个稳定的包,其中: 1)有3个包依赖它。 称X对这3个包负有责任。 2)X不依赖任何包,因此所有 X 外部影响都不会使其改变。 称X是无依赖的。
  106. 106. Y Y包就是一个不稳定的包,其中: 1)依赖于3个包,有3个外部更改源。 称Y是有依赖性的。 2)没有任何包依赖于Y。 称Y是不承担责任的。
  107. 107. 稳定性度量 一种度量“位置”稳定性的方法是,计算入、出包的依赖关系 数目 Ce I= ———— Ca + Ce 其中,• Ca 输入耦合度:指处于该包外部并依赖于该包内的 类的数目 • Ce 输出耦合度:指处于该包内部并依赖于该包外的 类的数目 • I :不稳定性。取值范围为[0,1]。 I=0 表示该包具有最大的稳定性 I=1 表示该包具有最大的不稳定性
  108. 108. 例如: pa pb q r s pc pd t u v 其中,pc外部有3个类依赖pc内的类,所以Ca = 3。此外,pc外部 有一个类被pc内的类依赖,所以Ce =1。I=1/4。 1)在C++中,这些依赖关系是通过#include语句表示的。 2)在Java中,可以通过计算import语句以及类的修饰名的 数目来计算度量I。
  109. 109. 注: 1)SDP规定一个包的I值应该大于它所依赖包的I值(即I值 应该顺着依赖的方向减少)。 2)该条原则并非要求所有的包都是稳定的。 如果一个系统的所有包都是最大程度稳定的,那么该系 统就是不能改变的。实际上,我们希望一些包是稳定的 而另一些包是不稳定的。 I=1 instable instable I=1 stable instable Flexible I=0 I=0 理想的包配置 违反了SDP
  110. 110. stable stable U U Uinterface Flexible Flexible 《interface C C 》 IU 产生违反SDP的原因 使用DIP,修正稳定性 1)创建一个接口类IU,并把它放入Uinterface中。确保接 口IU中声明了U要使用的所有方法。 2)让C从该接口继承。以解除stable对Flexible的依赖,并 使这2个包都依赖Uinterface。 结果:1) Uinterface非常稳定。(I=0),而Flexible仍保 持必须的不稳定性(I=1) 2)现在,所有依赖都顺着I减少的方向。
  111. 111. 第6条原则 稳定抽象原则(SAP) 包的抽象程度应该和其稳定程度一致。 1)解决的问题 一般来讲,应该把封装系统高层设计的软件放在稳定的包 中(I=0)。但如何使高稳定(I=0)的包又是足够灵活,可 以经受得住变化? 这就是提出本原则的动机! 2)基本思想 把包的稳定性和抽象性联系起来,规定: (1)一个稳定的包应该是抽象的,即应该包含一些抽象类, 这样的稳定性就可以进行扩展,既是灵活的,又没有过分 限制设计; (2)不稳定的包应该是具体的,其内部的具体代码易于更改
  112. 112. 与DIP原则的关系 (1)SAP和SDP结合在一起,形成了关于包的依赖倒置原则 (DIP)原则。 SAP规定,依赖应该朝着稳定的方向进行; 而SDP规定,稳定性意味着抽象性。 说到底,依赖应该朝着抽象的方向进行。 (2)DIP是一个处理类的原则,类没有灰度(the shades of grey)问题,即类要么是抽象的,要么不是。而SAP和 SDP的结合,允许一个包是部分抽象的,部分是稳定的。 (注:一个抽象类是一个至少具有一个纯接口的类,并且不能被 实例化。)
  113. 113. 抽象性度量 一种度量抽象性程度的方法是: Na A= —— Nc 其中,Na:包中抽象类的数目 Nc:包中类的总数 A:抽象性 可见,A的取值范围为0到1。0意味着包中没有任何抽象类; 1意味着包中只包含抽象类;
  114. 114. 抽象性与稳定性之间的关系 (0, 1) A (0, I (1, 0) 0) 可见,1)最稳定、最抽象的包位于(0,1)处; 2)最不稳定、最不抽象的包位于(1,0)处。 肯定地说,不能强制所有的包都位于(0,1)处,那么在A/I 图中,那些位置是合适的? 显然:1)(0,0)附近区域的包,是高度稳定且具体的包。 -这样的包不是灵活的,无法对它进行扩展,也很难
  115. 115. 例如,“数据库模式就处于这一区域。它具有易变性,而且还 是非常具体、高度依赖的。因此面向对象应用程序和数据库之间 的接口难于定义,数据库模式的更新是很痛苦的”。 2)(1,1)附近区域的包,是具有最大抽象性而没有依赖的 包。 -这样的包是无用的。--这一区域称为“无用区域”。 通过以上分析,可以得出:距离这2个区域最远的点的轨迹, 即连接点(0,1)和(1,0)的线段,称为包的主序列。 (0, (1, 1) 1) 无用区域 A 痛苦区域 (0, I (1, 0) 0)
  116. 116. 显然 1)位于主序列上的包既不是太抽象,因为它具有稳定性; 又不是太不稳定,因为它具有抽象性。既不是无用的, 又不是令人痛苦的 ; 就抽象性而言,它被其他包所依 赖; 就具体性而言,它又依赖于其他包。 2)包的最佳位置是2个端点处。但这样的包在整个项目中 一般均不大于50%,其他的包在该线段的附近就是很好 的。
  117. 117. 到主序列的距离 |A+I-1| 距离 D = ———— 2 取值范围为[0,-0.707] 规范化距离 D’ = |A+I-1| 取值范围为[0,1] 0 表示一个包正处在主序列上 1表示一个包到主序列的距离最远 注:实践中,使用D’要比D更方便一些。
  118. 118. 使用“到主序列的距离”这一概念的意义 1)可以全面分析一个设计和主序列的相符程度。其步骤是; 首先,计算每个包的D; 然后,对所有D值不在0附近的包进行复查和调整。 --这一分析有助于设计者确定 • 哪些包更容易维护 • 哪些包对变化更不敏感。 2)可以对设计进行统计分析。其步骤是: 计算设计中所有包D的均值和方差。显然,期望该设计的 均值和方差均接近于0。其中,方差可用来建立“控制限制”, 以标识那些与所有其他包显得“特别”的包。 A (0, 无用区域 1)• • • • Z=1 • • • • • Z=2 ••• • • • • • Z=2 • • • • •• • • • • •• Z=1 • I
  119. 119. 使用“到主序列的距离”这一概念的另一种方法 绘制出每个包D’值随时间的分布图,如下所示: 0.2 Package:Payroll D’ 0.1 • • • • • R1.0 R1.1 R1.2 R2.0 R2.1 包Payroll的D’值之时间分布图 从该图可以看出: 1)D’的控制阈值为:0.1; 2)一些“奇怪”的依赖关系在最近几次发布中已经蔓延到包Payroll中; 3)R2.1已经超出了规定的控制阈值。
  120. 120. 结论 1)依赖性度量的作用: 可以测量一个设计是否为“好”的依赖,以及抽象结构模式 的匹配程度。 2)使用: • 该度量不是万能的,它只是一种度量方法。 • 该度量可能只对某些应用是合适的,而对另一些是不合 适的。可以使用其他更好的度量方法,测量一个设计的质 量。
  121. 121. 谢 谢 !

×