北京大学研究生软件工程课程敏捷软件开发方法_第1页
北京大学研究生软件工程课程敏捷软件开发方法_第2页
北京大学研究生软件工程课程敏捷软件开发方法_第3页
北京大学研究生软件工程课程敏捷软件开发方法_第4页
北京大学研究生软件工程课程敏捷软件开发方法_第5页
已阅读5页,还剩132页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、敏捷软件开发方法,(一) 引言 1、在敏捷开发中,所涉及的原则分为: (1)开发原则 共12条。这些原则是从“实践”的角度给出的。 用于项目的组织 (2)设计原则 共11条。这些原则是从设计的角度给出的原则。 用于开发人员的行为指导。 其中 为了解决所谓的软件“腐化”问题,提出了5条。 为包的设计(集成技术),给出了6条。,2、原则提出的基本思想源 软件之美,在于: 1)它的功能 对于用户来说,通过直观、简单的界面呈现出恰当特征的程 序就是美的; 2)它的内部结构 对于软件设计者来说,通过简单、直观的划分,使其具有最 小耦合的内部结构就是美的; 3)团队创造它的过程 对于开发人员和管理者来说,

2、每周都会取得重大进展,并生 产出无缺陷代码的具有活力的团队过程就是美的。,实现软件之美的途径-敏捷开发 所谓敏捷开发,就是一种面临迅速变化的需求快速开发 软件的能力。 获取这一能力,需要: 1)使用一些实践,以提供必要的纪律和反馈; 2)使用一些设计原则,以保持软件是灵活的、可维护的; 3)掌握一些设计模式,以便针对特定问题权衡这些原则。,3、敏捷开发(主要包括敏捷实践和敏捷设计) 敏捷联盟的宣言,具体表达了敏捷开发的基本思想。,敏捷联盟宣言 我们正在通过亲身实践,并通过帮助他人的实践,揭示更好的软件开发方法。这些工作使我们认识到: 个体和交互 胜过 过程和工具 可以工作的软件 胜过 面面俱到

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,二、设计原则 1、基本思

4、想 敏捷设计是一个过程,而不是一个事件。该过程是一个不 断应用原则、模式和实践,来改进软件结构和可读性的过程。 其目标是,保持系统设计在任何时间都尽可能的简单、干净 和富有表现力。 若把敏捷性看作是以增量方式构造软件的话,就要研究: 1)如何设计软件? 2)如何确保软件具有灵活性、可维护性和可复用性的良好 结构? 3)如何以微小增量的方式构造软件并避免无用的代码“碎片”和返工? 4)如何避免忽略全局视图?,敏捷开发思想的来源: “ 满足工程设计标准的唯一文档是源代码清单”。 -Jack Reeves 1992 ,“C+ Journal” 在这篇文章中, Jack Reeves对C+的流行,思考

5、了一个问 题,即什么是真正软件设计。按着他的观点: 1)“ 满足工程设计标准的唯一文档是源代码清单”。 他通过仔细审视软件开发的生命周期,编译器和连接器是 实现软件构建的基本工具。其廉价程度是令人难以置信的。 并且,随着计算机速度的加快,软件构建将变得更加廉价。 这是软件业和传统制造业之间的一个重要区别! (设计团队设计文档;制造团队构建产品。 设计与制造独立;人员技能不同。),2)“设计实际软件的昂贵程度 是令人难以置信的, 之所以如 此,是因为软件是非常复杂的,并且软件项目的几乎所有 步骤 都是设计的一部分”。 (1)编程是一种设计活动,并且在编码显得有意义时,就 应立即进行编码。 其中:

6、 可以使用一些支持高层设计的结构图表、类图、状态图和 PDL等工具和表示法,提供必要的帮助,但它们都不是真 正意义上的软件设计(不符合严格的工程设计标准), 因此必须创建真正的软件设计,并使用某种编程语言来 完成之。,软件开发过程模型的不断演化,可以表明,支持在软件 生存周期的更早时期开始编码,以便使得验证和测试更 早地对设计进行验证和改进。并且,还意味着高层设计 人员很有可能参与详细设计。 编码比我们所说的一般设计更具有表现设计意义。因为 其中会揭示出一些疏漏和额外的设计需求。因此编码发 生的越早,其设计就越好。,(2)测试/调试是一种设计活动,就软件开发来说,它们相 当于其它工程学科中的验

7、证和过程改进。 (3)其他设计活动,包括高层设计、模块设计、体系结构 设计等。其中,所使用的许多不同的软件设计表示法, 它们不是软件设计,但可以作为辅助文档和工具,来 简化设计过程。具体地说,辅助文档的主要用途为: 从问题空间中捕获并记录一些在设计中不能直接使用的 重要信息。(这就是我们所说的需求规约) 软件设计要创建一些软件概念来对问题空间中的概念进行建模, 该过程需要得到一个对问题空间的理解。通常,这一理解包含 一些最后还不会直接建模到软件空间中的信息。但它们却有助于 设计人员确定什么是本质的概念,如何对它们给出很好的模型, 以便以后对模型的更改。, 记录设计中某些方面的重要内容。而这些内

8、容是很难作为 注释包含在代码中。这些内容可以是高层的,也可以是低 层的。显然,图形是描述它们的最好方式。 体系结构文档是其中最重要的一个辅助文档。 一个好的 OO体系结构通常可以使用几幅图和十几页文字 描述出来。 但必须集中于设计中的关键类和关系。 必须由人来编写这样的文档和维护之。 最好在源代码完成后,或在编写源代码的同时编制这样 的文档,而不是在源代码编写之前进行。,3)目前软件开发仍然还是一门工艺,而不是一门工程学科。 主要 因为在验证和改善设计的关键过程中,缺乏所需要的 严格 性。 4)软件开发的真正进步依赖于编程的进步,这意味着依赖于 编程语言的进步。回到本文开始提出的思考,可以回答

9、为 什么C+最为流行,其根本原因是,它是一种直接支持软 件设计的主流编程语言。,拙劣设计的表征 1)僵化性(Rigidity):是指难于对软件设计进行改动, 即使是简单的改动。 表现:如果一个改动会导致有依赖关系的连锁改动,那么 设计就是僵化的。必须改动的模块越多,其设计就 越僵化。 2)脆弱性(Fragility):是指在进行一个改动时,程序的许 多地方就可能出现问题,即设计易于遭受破坏。 并且,往往是出现新问题的地方与改动的地方并 没有概念上的关联。,3)粘固性(Immobility):是指设计中包含了对其它系统有用 的部分,但要想把这些部分分离出来所需要的努力和 风险是巨大的,即设计难于

10、复用。 4)粘滞性(Viscosity):具有以下2种表现形式: 软件粘滞性:是指当面临一个改动时,那些可以保持系 统设计的方法比那些破坏设计的“生硬”方法更难应用,即难于做正确的事情。 环境粘滞性是指环境的迟钝和低效。例如编译时间长。- 也是难于做正确的事情。,5)不必要的复杂性(Needless Complexity):是指设计中包含 了当前没有用的成分,即过分设计。 危害:这种设计,一方面使软件变得复杂,另一方面使软件 难于理解。 6)不必要的重复( Needless Repetition):是指滥用“剪切”和粘贴”鼠标操作。 “剪切”和“粘贴”操作也许是有用的文 本编辑操作,但却是灾难

11、性的代码编辑操作: 往往是开发人员忽略了抽象。从而使系统不易理解; 软件中的重复代码,使系统的改动变得更加困难, 不易系统的维护。,7)晦涩性(Opacity):是指模块难于理解。代码随时间而不 断演化,往往会变得越来越晦涩。 办法:为了减少代码的晦涩性,需要持续地保持代码清晰 和富有表现力。,引起软件腐化的原因: (1)需求没有按照初始设计所预见的方式进行变化,从而 导致设计的退化;(但不能因为退化而责备需求的变化) (2)改动的要求是紧迫的,并且开发人员对于原来的设计 思路并不熟悉,虽然可以进行一些改动,但却以某种方式 违反了原来的设计,并随着时间的推移,这些违反不断积 累,就使软件出现“

12、臭味”。,防止软件腐化的基本途径 依赖变化来获取活力 (1)团队几乎不进行预先(up-front)设计,因此不需要一 个成熟的初始设计; (2)团队使用许多单元测试和验收测试,支持系统的设计 尽可能的干净、简单,使设计保持灵活性和易于理解性; (3)灵活、持续地改进设计,以便使每次迭代结束时所生成 的系统具有满足那次迭代需求的设计。,2、设计原则 ( 共11条。从解决这些病症,给出了5条;从包的设计, 给出了6条。这些原则 支持开发人员的设计) 单一责任原则 开放-封闭原则 Liskov替换原则 依赖倒置原则 接口隔离原则 注:-软件工程一些原则的特例; -无条件地遵循这些原则是错误的做法;

13、-过分地遵循这些原则将导致不必要的复杂性。,(一)单一职责原则(SRP) 只有佛自己应当承担起公布玄妙秘密的职责。 -E.Cobham Brewer,1810-1897 英语典故字典,1898 (1)内容: 就一个类而言,应该仅有一个引起 它变化的原因。 这条原则被称为内聚性原则。 一般地,内聚性是指一个模块组成元素之间的功能相关性。 在本条原则中,把内聚性和引起一个模块或类的改变的 作用联系起来。,为了理解这一原则,首先要回答什么是职责? 在SRP中,把职责定义为:引起变化的理由 (a reason of change)。 根据这一定义,显然可知:如果能够想到多个(1)动机 来改变一个类,那

14、么这个类就具有多于一个的职责。 实践中,人们很习惯以“组”的形式来考虑类的职责。例如: 一个接口“调制解调器”有四个功能: interface modem public void dial (string pno); public void hangup ( ); public void send (char c); public void recv ( ); ,根据职责的定义,可以认为该接口有两个职责: 连接处理(public void dial (string pno); public void hangup ( );) 数据通信(public void send (char c); pu

15、blic void recv ( );) 这一接口“调制解调器”显然违背了SRP原则! (2)问题:是否将这2个职责分离? 这取决于应用程序变化的方式: 如果应用程序的变化会影响连接函数的声明,那么调用 send和recv就必须重新编译,因此应分离这两种职责。 如果应用程序的变化总是导致这两个职责同时变化,那么 就不必分离这两种职责。,(3)如何实现 耦合职责的分离? 就上一个例子而言,可以把2个职责分离为: 其中,可以把ModemImpiementation类看作是一个杂凑物 (kludge),通过分离它们的接口,解除耦合的概念(“连接”“通信”)。这样,使所有对该接口的依赖,都与 Mode

16、mImpiementation类无关。除了main外,谁也不知道它 的存在。,interfaceData Channal +send(:char) +recv( ):char,interfaceConnection +dial(pno:String) +hangup( ),Modem Implementation,(4)持久化问题-左图给出了一种常见的违反该原则的情况 其中,类Employee包含了业务 规则和持久性控制。这2个职责 在多数情况下不应该合在一起。 因为业务规则往往会不断的变化, 并且变化的原因也各不相同。 如何处理: 第一,测试驱动的开发实践,往往会避免这种情况发生,即 迫使对

17、这 2 种职责进行分离。 第二,如果出现了这种情况,可以使用FAADE和 PROXY 模式对设计进行重构,分离这 2 种职责。,Employee +CalculatePay +Store,Persistence Subsystem,附0:测试驱动的软件开发,如何看待单元测试? 单元测试是一种验证行为, 更是一种设计行为。同样 它更是一种编写文档的行为。 编写单元测试,可以避免相当数量的反馈循环,尤其是功能验证方面的反馈循环。,1、测试驱动的开发方法 1) 思考与问题: 如果在设计程序之前先设计测试方案,情况将会怎样? 如果能够做到除非缺少某一功能而导致测试失败,否则就 拒绝在程序中实现该功能,

18、情况将会怎样? 如果能够做到除非缺少某行代码而导致测试失败,否则就 拒绝在程序中增加哪怕一行的代码,情况又会怎样? 如果首先编写失败的测试,表明需要一项功能,然后增加 这一功能使测试通过,情况将会怎样? 以上的思考,将对编写软件的设计产生什么影响? 如果存在以上问题所涉及的测试,我们能够得到什么好处?,2) 测试先于设计的示例 一个简单的游戏程序,名为-“Hunt the Wumpus”。 该游戏的内容为: 玩家(WumpusGame)在洞穴中移动,在没有被Wumpus吃掉 前,设法杀死它。 其中:1)该洞穴由一系列通过过道相连的房间组成; 2)每个房间都有向东、向西、向南向北的通道; 3)玩

19、家通过告诉计算机要行进的方向而四处移动,以便 实现其自己的目标-设法杀死Wumpus。,在编写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)”。可以简

20、单、清晰地给程序指出一个好的结构。,通过这一测试可以看出: (1)没有引入“Room”类!仅使用整数表示房间。 作者认为:就游戏而言,连接(从一处到另一处)概念 比“房间”概念更重要。这就是一个意图。 这样,在早期阶段就阐明了一个设计决策。 (2)通过该测试,可以使编程人员了解该游戏程序是如何工 作的。 (3)根据这一简单的规格说明, 可以很容易地实现WumpusGame中的已经命名的方法, 可以很容易命名其它3个方向并实现之。,3)测试可以促使模块之间的分离 例如,一个薪水支付应用,在没有编码前,简单、快速地划了一个UML图:,CheckWriter +writerCheck( ),Payr

21、oll,Employee +caculatePay( ) +postPayment( ),Employee Database +getEmployee( )+putEmployee( ),其中, 类Payroll使用EmployeeDatabase获得Employee对象,并要求 Employee来计算自己的薪水。 接着,它把计算结果传给CheckWriter对象,产生一张支票。 最后,它在Employee对象中记录支付信息,并把Employee 对象写回到数据库中。,现在,要编写 规定Payroll对象行为的测试。其中需要 考虑的问题有: 使用什么样的数据库? Payroll对象需要从哪些数

22、据库中读取数据? 在测试前,需要把什么数据加载到数据库中? 如何检验打印出来的支票的正确性? 对于以上这些问题,可以使用MOCK OBJECT模式解决之。即 在Payroll类和它的所有协作者之间插入接口,创建实现这些 接口的测试桩。如下图所示:,interfaceCheckWriter +writerCheck( ),Payroll,interface Employee +caculatePay( ) +postPayment( ),interface Employee Database +getEmployee( )+putEmployee( ),PayrollTest,Mock Chec

23、kWriter,Mock Employee,Mock Employee Database,从上图可以看出, 现在Payroll类使用接口和CheckWriter、 Employee 、 EmployeeDatabase 进行通讯,创建了实现这些接口的Mock Objects。 PayrollTest对象对这些Mock Objects进行查询,以检验 Payroll对象是否正确地对它们进行了管理。,这一测试的设计思想是: 创建适当的Mock Objects,并把它们传递给Payroll对象, 告诉Payroll对象为所有雇员支付薪水,接着要求Mock Objects 去检查所有已开支票的正确性以

24、及所有已支付信息的正确性。 显然,这一测试所检查的都是Payroll应该使用正确的数据调 用正确的函数。它既没有真正地去检查支票的打印,也没有真 正地去检查真实数据库的正确刷新。 相反,它检查了Payroll类应该具有与它独立情况下同样的行 为。其中,不引入MockEmployee类,而是直接使用Employee类, 这是可以的,但使 Employee类显得复复杂了一些。,程序: Public void testpayroll() MockEmployeeDatabase db=new MockEmployeeDatabase(); MockCheckWriter w=new MockChec

25、kWriter(); Payroll p= new Payroll(db,w); p.payEmployees(); assert(w.checksWereWritenCorrectly(); assert(db.paymentWerePostedCorrectly(); ,在这一测试中,引入了Payroll类,这不但是为了测试,也是 给出了一个设计决策!实现了“处理”与环境(数据库,打印 机)的分离。这样就可以互换使用不同类型的数据库和打印 机,以便应用的扩展。,4)关于验收测试 类属:验收测试是用来验证系统满足客户需求的黑盒测试。 含义:验收测试是关于系统特征的可编译、运行的文档。 编写:

26、客户可以直接编写验收测试或与开发人员一起编写。 通常使用专为应用程序的客户创建的脚本语言来编写 验收测试。 意义:首先编写验收测试的行为,就必须在很高的系统框架 层对系统进行解耦合,以便使系统具有可测性。 问题:在迭代初期,往往会受到手工方式的诱惑-没有很好地激励解的耦合,如此时清楚必须进行自动化的验收测试,就会在非常不同的系统体系结构方面作出权衡,有利于作出优秀的系统体系结构的决策。,对开始问题的回答: 第一个影响是,程序中的每一项功能都有测试来验证其操作的正确性。这一套测试程序可以为以后的开发提供支持-可以对程序功能和结构的改变提供支持。 一个重要的影响是,首先编写测试,可以迫使我们使用不

27、同的观察点。其中,必须从调用的角度,来观察所要编写的程序。即在关注功能的同时,直接关注它的接口。编写一个便于调用的软件。 首先编写测试,可以迫使我们把程序设计为可测试的。 注:为了实现一个便于调用的、可测试的程序,就必须“简化”该程序与其环境的耦合。-实现结构美。 首先编写测试的一个重要效果是,测试可以作为一种无价的文档形式-是可编译的、可运行的。这样的测试可以帮助其他程序员如何使用代码。,结论 (1)单元测试和验收测试都是一种文档形式-是可编译的、 可 执行的;因此它是准确和可靠的。 (2)编写测试的语言是明确的,即程序员能够阅读单元测试, 客户可以阅读验收测试。 (3)测试套件越简单,就会

28、频繁地运行之。测试运行得越多, 就会越快地发现那些与测试不符的问题。 (4)测试最重要的好处是对体系结构的影响。因为对于一个 模 块或一个应用而言,越是具有可测性,其耦合关系就 越弱。,附1: FACADE模式 (1)该模式的目标是: 把某些策略施加到另外一组对象上. 从而实现了职责的分离 注:FACADE模式从上面施加策略, 因而使用这一模式是要 受 到限制的。 应对的情况: 当要为一组复杂而全面的接口的对象(例如Java.sql包) 提供一个简单且特定的接口时,可以使用这一模式.,DB +store(ProductData) +getProductData(sku) +deleteProd

29、uctData (sku),Application,ProductData,Java.sql,Connection,Statememt,Driver Manager,ResultSet,Prepared Statememt,SQLException,(2)该模式所具有的结构:例如,其中, DB是一个FAADE类(呈表类),特定于 productData, 提供了一个非常简单 的 接口。Application不必了解Java.sql 的细节,把Java.sql包所有的复杂性隐藏在一个非常简单的 接口中. 对Java.sql包的使用,DB类向该包施加了许多策略.例如 如 何初始化和关闭数据库连接,

30、如何将ProductData的成员 变量转换为数据库字段,如何构造合适的查询和命令来 操纵数据库. 使用FAADE模式,意味着开发人员接受了有关DB的 约定,即用户(Application)的任意部分的代码都必须通过 这样的DB来访问Java.sql包-受限的方式,不能越过 FAADE类而直接访问Java.sql。 注意:基于这一约定,使DB成为Java.sql包的唯一代理 (broker).,附2:PROXY模式 1)问题: 软件开发中存在许多障碍。当把数据移到数据库中时,要跨越数据库障碍; 当把消息从一台计算机发送到另一台计算机时,要跨越网络障碍。 跨越这些障碍可能是复杂的。 PROXY模

31、式 有助于在跨越这类障碍,并 使程序仍然保持关注本身要解决的问题。,interface Product,DB,delegates,2)PROXY模式及工作原理 如下所示: (以购物车系统为例),Product DB Proxy,Product Implementation,Proxy模式的静态结构,每个要被代理的对象(“产品”)被分为3个部分: 第一部分是一个接口,它声明了客户端要调用的所有方法; 第二部分是一个类,该类在不涉及数据库逻辑的情况下实 现了该接口中的方法; 第三部分是一个知晓数据库的代理。,其中 (1)对于product类,通过用一个接口实现了对它的代理。如 上所述,该接口有pr

32、oduct类的所有方法,这些方法从数据 库中取出产品。为此,它创建一个ProductImplementation 实例,然后再把消息委托给这个实例。 下面Proxy模式的动态结构展示了该模式是如何工作的:,Product DB Proxy,DB,Product Implementation,getPrice(),price,retrieveProduct(sku),product,getPrice(),price,Proxy模式的动态结构,工作过程: 客户向一个它认为是product、但实际上是productDBProxy 的对象发送getPrice消息。 ProductDBProxy 从数据

33、库中创建ProductImplementation, 然后把getPrice方法委托给它。 (2)Proxy模式的优点:客户和ProductImplementation都不知 道所发生的事情。数据库在这两者都不知道的情况下被插 入到应用程序中。因此,该模式的最大好处是,可以实现 一些重要关系的分离(separation of concerns)。 就这一例子而言,实现了业务规则和数据库的分离。这是 当前最流行的一种保持业务规则和实现机制分离的方法。,结论 1、单一职责原则(SRP)是最简单的一个原则,但也是最难 运用的原则之一; 2、在设计实践中的问题: 人们会自然地把职责合在一起。这是导致产

34、生违背这一原 则的基本原因; 3、软件设计指导之一: 在软件设计的整个过程中,一个主要工作,就是不断地发 现职责,并把这些职责进行分离。 本质上,其它原则都是以这种方式或那种方式回答如何发现 职责,并进行分离。,(二)开放-封闭原则(The Open-Closed Principle,OCP) 1、内容:软件实体(类、模块、函数等)应该是可扩展 的,但是不可修改的。 1)“对于扩展是开放的”(open for extention) 这意味着模块的行为是可以扩展的。换言之,可以改变模 块的功能。 2)“对于改变是封闭的”(closed for modification) 这意味着对模块行为改变时

35、,不必改动模块的源代码或 二进制代码。模块的二进制可执行版本,无论是可链接的 库、DLL或Java的.jar文件,都无需改动。,2、实现这一原则的基本思想和机制: (1)基本思想: 关键的是抽象! (2)机制:继承 不允许修改的模块,通常被认为是具有“固定”行为的模块。抽象基类以及可能的派生类,就能够描述一组任意可能行为的抽象体。 应用模块可以操作一个抽象体。由于该模块依赖一个“固定”的抽象体,因此对于更改来说,可以认为该抽象体是 封闭的;但通过该抽象体的派生,可以扩展该模块的行 为,这样可以认为该抽象体是开放的。,STRATEGY模式:既开放又封闭的Client,Client,interfa

36、ce Client Interface,Server, 通过委托方式,实现开放-封闭原则(OCP)的结构:,Client,Server,由于, Client 类和Server类都是具体类, Client类是既不开放的 又不封闭的类,( 3 ) 实现该原则(OCP)的结构 例如:,该接口体现了抽象!,附3: STRATEGY模式 1)所要解决的问题: STRATEGY模式可以用来 分离通用算法,并允许高层算法 独立于它的具体实现得以复用. 2) 特征: STRATEGY模式是使用委托来解决问题,并且 STRATEGY模式还允许具体实现细节独立于高层的算法 而得以复用。(不过需要付出一些额外的复杂

37、性、内存和运行时 间为代价.),3) 途径 为了使用委托来实现通用算法与具体实现的分离,该模式倒置了通用算法和具体实现之间的依赖关系。例如:,Application Runner +run,interface Application +init +idle +cleanup +done:boolean,ftocStrategy,具体地说: (1)不是把通用算法(+run)放在一个抽象基类中,而是放在名为 ApplicationRunner的具体类中。 (2)把通用算法必须要调用的抽象方法(例如: +init ,+idle等 )定义在名为 Applicatinn的接口中。 (3)从这个接口派生出

38、 ftocStrategy,并把它传给ApplicationRunner。 之后, ApplicationRunner就可把具体工作委托给该接口来完成。,该例的程序可以是: ApplicationRunner.java Public class ApplicationRunner private Application itsApplication=null; public ApplicationRunner(Application app) itsApplication=app; Public void run() itsApplication.init(); While(!itsAppli

39、cation.done() itsApplication.idle() itsApplication.cleanup ,通用算法(+run),Application.java Public interface Application public void init(); public void idle(); public void cleanup(); public boolean done(); ,通用算法必须要调用的抽象方法,ftocstrategy.java import java.io.*; Public class ftocstrategy implements Applicat

40、ion private InputStreamReader isr; private BufferedReader br; private boolean isDone=false; public static void main(Sring args) throws Exception (new ApplicationRunner(new ftosStrategy().run(); public void init() isr=new InputStreamReader(System.in); br=new BufferedReader(isr); ,从接口Application派生出 ft

41、ocStrategy,,把ftocStrategy传给ApplicationRunner。,Init的实现,public void 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=”+ce

42、lcius); public void cleanup() System.out.println( “ftoc exit”); public boolean done() return isDone; ,Idle的实现,Cleanup的实现,private String readLineAndReturnNullIfError() String s; try s=br.readLine(); catch(IOException e) s=null; return s; ,综上,ApplicationRunner就可把具体工作委托给该接口来完成。,其中, ClientInterface类是一个拥

43、有抽象成员的抽象类。 Client 类使用这个抽象类,但Client 类的对象却使用Server类的派生类的对象。 (1)封闭性的体现:如果希望Client 对象使用一个不同的服务器类,那么只需从ClientInterface类派生一个新的类,而无需对Client 类进行任何改动。 (2)开放性的体现:如果Client 类需要实现一些功能,可以使用ClientInterface的抽象接口来描述这些功能。 ClientInterface的子类型可以以任何方式来实现这个接口。这样,就可以通过创建ClientInterface的新的子类型的方式来扩展、更改Client 中指定的行为。,其中,Poli

44、cy类具有一组实现了某种策略的共有函数。这些策略函数使用一些抽象接口描述了一些要完成的功能。但在这个结构中,这些抽象接口是Policy 类的一部分。(它们在C+中表现为纯虚函数,在Java中表现为抽象方法。) Policy类中的这些函数在Policy的子类型中予以实现。这样,可以通过从Policy类派生出新类的方式,对Policy中指定的行为进行扩展或更改。(体现了开放性和封闭性!),Policy +PolicyFunction() -ServiceFunction(),Implementation -ServiceFunction(), 通过抽象基类的方式,实现OCP的另一结构:,Templ

45、ate Method模式:既开放又封闭的基类,附4: (1) TEMPLATE METHOD模式 该模式把所有通用代码放入一个抽象基类的实现方法中,而 将所有实现细节都交付给该基类的抽象方法。如下所示:,基类A 方法:F 通用算法 方法1(抽象方法):实现细节 方法2(抽象方法):实现细节 ,(2)TEMPLATE METHOD模式和STRATEGY模式的比较 TEMPLATE METHOD模式和STRATEGY模式都可以 用来把一个功能的通用部分和实现细节清晰地分离开来。 两个模式是满足OCP原则最常用的方法。其途径是,遵循 倒置依赖原则(DIP),使通用算法不依赖具体的实现,并 使通用算法

46、和具体实现都依赖抽象. 2种模式的区别:继承和委托 尽管TEMPLATE METHOD模式和STRATEGY模式所要解决的问题是 类似的,而且可以互换使用. 但 TEMPLATE METHOD模式使用继承来解决问题, 而STRATEGY模式是使用委托来解决问题.,结论 1、在许多方面,开发-封闭原则(OCP)都是面向对象设计的 核心。遵循这一原则,可以带来很大的好处: -灵活性、可复用性以及可维护性。 Ivar Jacobson曾经说过:“任何系统在其生存周期中都会 发生变化。如果期望开发的系统不会在第1版本后就被抛弃, 必须牢牢记住这一点。” 2、不是使用了面向对象语言就是遵循了这一原则。

47、3、对应用程序中的每一部分肆意地使用抽象,同样不是一个 好的主意。正确的做法是,开发人员应该仅对呈现频繁变 化的那些部分做出抽象。其中,“拒绝不成熟的抽象与抽象本身一样重要的”。,(三)Liskov替换原则( LSP) OCP所隐含的主要机制是抽象和多态(polymorphism)。 在C+和Java这类语言中,支持抽象和多态的关键机制是 继承(inheritance)。正是使用了继承,才可能创建实现其基 类中抽象方法中的派生类。 然而, 是什么设计原则支配这种特殊的继承用法? 最佳的继承层次的特征是什么? 怎样的情况会使创建的类层次陷入不符合OCP? 以上这些问题正是LSP原则要回答的。,1

48、、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来说是脆弱的。,2、例子 违反 LSP,常常以违反OCP的方式使

49、用运行时的类型辨别(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; ;,struct

50、Square:public Shape Sqaure():Shape(Sqaure) ; void Draw() const; Point itsTopLeft; double itsSide; ; void DrawShape(const Shape ,运行时的类型辨别,结论:这样就违反了LSP,从而使DrawShape违反了OCP。 原因是:它必须知道Shape类所有的派生类,且每次创建 一个Shape的派生类都必须要更改它。这是因为: Square类和Circle类是Shape的派生类,具有函数Draw() 但没有重写(override) Shape 中的函数。由于Square类 和Ci

51、rcle类不能替换Shape,所以DrawShape函数必须检查输入 的Shape对象,确定它的类型,继之调用函数Draw()。 Square类和Circle类不能替换Shape,这样就违反了LSP,从而 使DrawShape违反了OCP。,还有其它一些方式,可以引发违反Liskov替换原则。例如: is A关系的使用 -没有把子类中那些不具有多态性的函数声明 为虚函数 基于契约设计的使用 -派生类违反了有关基类的约定 在派生类的方法中填加了其基类不能割舍的异常。,结论: 1、LSP是导致违背OCP的主要原因之一。 正是子类型的可替换性,才使使用基类的模块在无须改 变的情况下可以予以扩展。 2

52、、这种可替换性必须是开发人员可以隐式依赖的东西。因 此,如果没有显示地强制基类类型的契约,那么代码就 必须良好地并显式地表达出这一点。 3、“is-A”的含义过于宽泛,以至于不能作为子类型的定义 子类型的正确定义是“可替换性的”,这里的可替换性可以通过显式或隐式的契约予以定义。,(四)依赖倒置原则( DIP) 1、内容: a. 高层模块不应该依赖低层模块。二者都应该依赖抽象。 b. 抽象不应该依赖细节。细节应该依赖抽象。 该原则是框架设计的核心原则 2、层次化 Booch曾经说过:“所有结构良好的面向对象构架都具有清晰的层次定义,每个层次通过一个定义良好的、受控的接口向外提供一组内聚的服务。”

53、,这一结构存在一个潜伏的错误特征: (1)Policy layer对于其下的任一改动,包括对 Utility Layer 的改动,都是敏感的。 (2)这种依赖是可传递的。 关于这一结构的评价:这一结构是不好的!,Policy Layer,Mechanism Layer,Utility Layer,如果对以上这句话只是简单地予以理解,就有可能会出现以 下结构:,Policy,Mechanism,Utility,就面向对象技术而言,层次化的合适模型应该是:,Policy Layer,interfacePolicy Service Interface,Mechanism Layer,interfac

54、e Mechanism Service Interface,Utility Layer,每个较高层次为它所需要的服务声明一个抽象接口,较低层次实现这些抽象接口; 每个较高层次都通过抽象接口使用下一层。 这样 1)高层就不依赖低层,而低层则依赖高层; 2)不仅解除了其中的传递依赖关系,而且还解除了高层与其它层的依赖关系。,倒置的接口所有权问题 是著名的Hollywood 原则“Dont call us, well call you.” 即低层模块实现了在高层模块中声明并被高层模块调用的 接口。 依赖于抽象 是解释DIP规则的一个启发式规则。该规则建议不应该依赖 具体类-即程序中的所有依赖关系都应

55、该终止于抽象类或接口。 根据这个启发式规则,可知: 任何变量都不应该持有一个指向具体类的指针或引用; 任何类都不应该从具体类派生; 任何方法都不应该覆写它的任何基类中已实现的方法。,实践中几种具体情况的说明: 1)在编程中,有时必须要创建具体类的实例,而创建这些 实例的模块将会依赖它们。这说明程序中一般都会出现违反启 发式规则的情况。 2)如果一个具体类不太会改变,且不会创建其它类似的派生 类,那么依赖它也不会造成损害。这说明对那些稳定的具体类 而言,启发式规则似乎不大合理。 3)如果具体类是不稳定的,且还不想直接依赖之,则可以把 它们隐藏在抽象接口之后,以隔离它们的不稳定性。 4)依赖倒置规

56、则可应用于任何存在一个类向另一个类发送消 息的地方。,3、违反依赖倒置原则的例子: 其对应的Java : public class Button provide Lamp itsLamp; public void poll() if (/* some condition */ ) itsLamp.turnOn(); ,Button +poll(),Lamp +turnOn()+ turnOff(),这是一个以Button控制Lamp的系统。其中,Button类直接依赖Lamp对象,这个设计不能让Button控制其它设备。 该设计方案违反了DIP,即应用程序的高层策略没有与低层策略相分离,自然就使抽象依赖于具体细节。 什么是高层策略?它是应用的抽象,是那些不随具体细节而改变的“真理”。它是系统内部的系统-隐喻(metaphore).,Button +poll(),interfaceButtonServer +turnOn() + turnOff(),其中,由接口ButtonServer提供一些抽象方法,Button可以使用这些方法对有关设备进行控制。由Lamp来实现 接口ButtonServer。 这样的设计就具有很好的灵活性。但问题是:Lamp不可能还受其他类的控

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论