《2022年2022年接口设计 .pdf》由会员分享,可在线阅读,更多相关《2022年2022年接口设计 .pdf(7页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、拥抱变化:敏捷设计从理论到实践温 昱本文发表于程序员2004 年第 11 期如何应付软件开发中的“变化”,一直是近年来备受软件企业关注的问题。敏捷方法的兴起,更是为“随需应变”带来了一股强劲的浪潮。本文从理论和实践两方面,和大家分享笔者在敏捷设计方面的心得。首先,以一种全新的角度考察耦合,并将其表述为良性依赖原则;然后通过应用实例,说明该原则如何和著名的“面向对象设计5 大原则”结合,来“务实地应付变化”;最后从应付变化的角度,对各原则做综合总结。需 要 说 明 的 是,本 文 采 用“良 性 依 赖 原 则”的 叫 法,是 出 于 和 依 赖 倒 置 原 则(Dependency-Inver
2、sion Principle)的叫法保持一致的目的;由于“耦合”和“依赖”是一对使用都非常广泛的同义词,所以叫做“良性耦合原则”也是可以的。一、一、理论篇1、换个角度考察依赖1)依赖的概念依赖(Dependency):两个元素之间的一种关系,其中一个元素变化,导致另一个元素变化。依赖的同义词:耦合(Coupling),共生(Connascence)。依赖的危害:如果被依赖元素发生变化,可能引起另一个元素不得不变化。2)从会不会造成“实际危害”的角度考察依赖关于依赖,已经研究得很多了:从依赖程度的大小角度考察之,有了耦合度相关理论;从依赖产生的原因角度考察之,有了静态共生性、动态共生性和差异共生
3、性的相关理论;不一而足。是的,如果被依赖元素发生变化,可能引起另一个元素不得不变化,这就是依赖的危害。但是,如果被依赖元素不发生变化呢?答案是不会造成危害!于是,“冤案”产生了:由于需求分析上的偏差,设计中“在理论上”很稳定的耦合度低的依赖,可能“在实际中”恰恰是给我们造成危害的家伙;相反,“在理论上”声名狼藉的耦合度高的依赖,“在实际中”也可能并不给我们造成任何危害。于是,我们很自然地想到,区分依赖的“实际危害”和“理论危害”是有实践意义的。下面,从会不会造成“实际危害”的角度考察依赖,将其分为良性依赖和恶性依赖两种类型:恶性依赖:被依赖的元素“在实际中”,而不是“在理论上”,是“易变的”良
4、性依赖:被依赖的元素“在实际中”,而不是“在理论上”,是“不易变的”2、良性依赖原则不会“在实际中”造成危害的依赖关系,都是良性依赖;依赖的“理论危害”不一定成为“实际危害”,反之亦然。这就是良性依赖原则。依赖是不可避免的,重要的是如何务实地应付变化。这就是良性依赖原则要做的。1)依赖是不可避免的OOD 的实质,简而言之,就是妥善地为多个类进行职责分配,使这些类相互协作而构造起完成特定功能的系统。在软件设计中,依赖是不可避免的,就象人类社会不可能没有人与人之间的协作与依赖一样,这一点其实是不言自明的。2)重要的是如何务实地应付变化需求改变时常发生,而良性依赖是那些不会“在实际中”造成危害的依赖
5、关系,所以,良性依赖是相对的需求改变可能使先前不易变的元素变得易变起来,从而良性依赖也变成了恶性依赖。Robert C.Martin在敏捷软件开发中提供的“只受一次愚弄”的策略很精辟。在我们最初编写代码时,假设变化不会发生;这时的设计很简洁,但对当时的需求,却是有效的、“不多不少的”。当变化发生时,我们就通过创建良性依赖,来隔离以后发生的同类变化;一般认为要通过创建抽象来隔离变化,而本文务实地认为只要是“不易变的”元素就可以。二、二、实践篇下面,通过几个应用实例,说明良性依赖原则如何和著名的“面向对象设计5 大原则”名师资料总结-精品资料欢迎下载-名师精心整理-第 1 页,共 7 页 -结合,
6、来“务实地应付变化”。1、第一个例子需求改变引起良性依赖变成恶性依赖需求改变了,原先的良性依赖变成了恶性依赖,但我们“只受一次愚弄”。比如,在开发一个需求跟踪工具的时候,起初可能仅需要支持保存为专有格式的“项目”文件,但后来又需要支持导出为HTML格式的网页。让我按照敏捷软件开发过程,来讲述这个故事:最开始的设计如下图所示,CReqMatrixDoc 调用 CProjectSaver来保存自己。此时,所有需求就是支持“保存为专有格式的项目文件”,而且我们并没有预见到将来还需要以更多的形式保存,所以类 CProjectSaver此时是“不易变的”,CReqMatrixDoc 对 CProject
7、Saver的依赖是良性依赖,整个设计也是个“稳定的”设计。顺便说明,按照开放-封闭原则(Open-Closed Principle),这并不是一个好的设计;但按照当前的需求,这个设计却“不多不少”刚刚好,因为当前它是满足良性依赖原则的。后来需求发生了变化,这个工具需要支持“导出为HTML格式的网页”的特性。是的,这个需求不管是客户新提出来的,还是设计人员在上一个迭代有意忽略了,总之在这个迭代周期需求发生了变化。于是,设计人员意识到,需求跟踪工具可能需要支持多种保存策略;如果不改变原来的设计,那么CProjectSaver就是“易变的”,因为它要支持可能不只一种新的保存策略。好了,如下图所示,设
8、计虽然没有改变,但由于需求的改变,原来设计中的良性依赖,现在变成了恶性依赖,这意味着CReqMatrixDoc可能也要随着CProjectSaver的改变而改变,这不是一个灵活的设计。是的,代码出现了臭味(Smell),需要重构(Refactoring)。让我们谨遵Martin Fowler的教诲不要将重构和添加新功能同时进行这一步我们仅进行重构。我们要做的就是去除这个恶性依赖,采用依赖倒置原则(Dependency-Inversion Principle)惯用的“用两个抽象依赖代替一个具体依赖”策略,重构之后的设计如下图所示。我们引入了一个接口CDocSaver,然后让 CProjectSa
9、ver实现这个接口。一个设计良好的接口无疑是“不易变的”,所以不管是CReqMatixDoc 对 CDocSaver 的调用,还是 CProjectSaver对 CDocSaver 的实现,都是良性依赖。重构完毕。名师资料总结-精品资料欢迎下载-名师精心整理-第 2 页,共 7 页 -哈,新的设计非常易于扩充,我们只需新写一个CHtmlSaver 来实现接口CDocSaver,就离支持“导出为HTML 格式的网页”不远了,如下图所示。咦,原来是策略模式。策略(Strategy)模式关键字:算法族。支持变化:它使得算法可以独立于使用它的客户而变化,多个算法之间也可以相互替换。2、第二个例子隔离第
10、三方SDK可能造成的冲击恶性依赖“作恶多端”。当恶性依赖中的被依赖元素变化时,依赖它的元素也可能要跟着变化;如果后者元素又在其他依赖关系中担当“被依赖元素”的角色,可能还会引起别的元素变化;这样,影响就会传播到很大的范围。有时候,去除恶性依赖的代价比较大;还有时候,恶性依赖在所难免;我们应当如何?答案是,不去追求完美,而是务实地用良性依赖隔离恶性依赖造成的危害。就让我来举个极端的例子吧第三方SDK。我们要开发的是压缩工具,我们不可能漠视现有的第三方SDK的存在,它们的诱惑实在太大了。在第一个迭代周期,要支持Zip 压缩格式,我们决定采用著名的Info Zip 开发包。Info Zip开发包并不
11、在我们的控制之下它的接口可能发生改变,当我们要使用更新的名师资料总结-精品资料欢迎下载-名师精心整理-第 3 页,共 7 页 -包时,我们可能面临不得不改动分散在很多类中的Info Zip使用代码的问题。所以我们要隔离这个我们控制不了的变化对,就用Adapter模式引入一个CInfoZipAdapter类来包装 Info Zip,如下图所示。这样,在 Info Zip 包升级时,我们仅需改动CInfoZipAdapter的实现就可以了。这个 CInfoZipAdapter并不是一个抽象类,所以当前的设计并不满足依赖倒置原则(Dependency-Inversion Principle)推崇的“
12、依赖于抽象”的要求;但client对CInfoZipAdapter的依赖关系是稳定的良性依赖,我们完全可以安心地远离过度设计(Over-engineering)。适配器(Adapter)模式关键字:已存在/不可预见,复用。支持变化:由于 Adapter 提供了一层间接,使得我们可以复用一个接口不符合我们需求的已存在的类,也可以使一个类(Adaptee)在发生不可预见的变化时,仅仅影响Adapter 而不影响 Adapter 的客户类。谨遵敏捷宣言“经常性地交付可以工作的软件”的教诲,第一个迭代周期过后,我们发布了压缩工具的一个可以工作的版本。第二个迭代周期,我们需要使用另外一个第三方的开发包来
13、支持新的压缩格式。显然,我们不应当让CInfoZipAdapter承担多于一个的职责,还是需要先重构。引入了接口CCompressAlgo 供“外部”调用,如下图所示。重构完毕,可以添加新功能了。从 CCompressAlgo“接口继承”出来一个COtherAdapter来封装另一个第三方SDK,如下图所示。哈,基本满意:(多个)client对 CCompressAlgo的良性依赖,使client的代码相当稳定;所有第三方SDK的不可控制的变化因素,都被妥善隔离。名师资料总结-精品资料欢迎下载-名师精心整理-第 4 页,共 7 页 -3、第三个例子对具体类的良性依赖良性依赖可以是对抽象基类的依
14、赖,也可以是对具体类的依赖。其实,这种对具体类的良性依赖的例子是很多的,比如设计模式 和敏捷软件开发中Facade模式的相关例子。下面,笔者举一个基于组件开发的例子,在“组件重用”这种“黑盒重用”日益盛行的今天,也许更具现实意义。好的组件库,都恪守接口隔离原则(Interface-Segregation Principle),以达到“不应该强迫客户依赖于它们不用的方法”的目的。这其中包含了基于角色的设计(Role based Design)的思想:协作被定义为“多个对象为了完成某种目标而进行的交互”;角色被定义为“特定协作中的对象的抽象”,它“仅定义了对象特征的一个对某协作有意义的子集”;协作
15、和角色的概念和现实世界很接近,我们很容易通过已有角色的组合来构造新的协作,以完成新的功能。好了,看我们的需求:在多个XML 文件中,查找那些包含特定名字的元素的文件。比如,我们可能希望知道哪些XML 文件中包含名为book的元素,这在 XML 网页越来越多的今天,对搜索引擎无疑是很有用。我们使用微软的DOM 组件来实现:CXmlFileSearcher代表一个高层模块,它的职责就是上面描述的需求;而具体的打开 XML 文件的工作,它交给 IXMLDOMDocument来做;具体的读取 XML 文件中的每个元素的名字的工作,交给 IXMLDOMNode来做;它是个典型的Facade模式,如下图所
16、示。外观(Facade)模式关键字:子系统,高层接口。支持变化:它实现了子系统与客户之间的松耦合关系,而子系统内部的功能部件往往是紧耦合的。松耦合关系使得子系统的组件变化不会影响到它的客户。值得说明的是,按照依赖倒置原则(Dependency-Inversion Principle),应当做到“高层 模 块 不 应 该 依 赖 于 低 层 模 块,二 者 都 应 该 依 赖 于 抽 象”。就 是 说,我 们 应 当 为CXmlFileSearcher提供一个抽象基类,供外部的高层模块client调用,这在当前的需求之下,未免有过度设计(Over-engineering)之嫌。这也正好体现了良性
17、依赖原则的“务实”优点。当然,我们并不是一味地回避使用抽象基类的“抽象耦合”。这不,没过多久,新需求出现了,不仅要搜索元素名,还要搜索属性名和文本格式的内容。还是首先仅做重构,引入抽 象 基 类 CSearcher,原 先 在 CXmlFileSearcher中 实 现 的 搜 索 策 略 移 到 具 体 类CEleNameSearcher中,如下图所示。需要特别说明的是,现在引入抽象和上一步引入抽象,是完全不同的两回事;现在引入抽象是基于实实在在的需求,而上一步引入抽象是基于猜测,其接口很可能不能满足刚刚提出来的新需求。名师资料总结-精品资料欢迎下载-名师精心整理-第 5 页,共 7 页 -
18、重构完毕,可以增加新功能了。运用策略模式,将搜索属性名和文本格式的内容分别实现作具体类 CAttrSearcher和CTextSearcher,如下图所示。三、三、总结篇“依赖”是和“变化”紧密联系在一起的概念。由于依赖关系的存在,变化在某处发生时,影响会波及开去,造成很多修改工作,这就是依赖的危害。可以说,变化是始作俑者,依赖是助纣为虐。我们可以不去拥抱变化吗?不可以。未来将越来越不可预测,这是新经济最具挑战性的方面之一。商务和技术上的瞬息万变会产生变化,这既可以看作要防范的威胁,也可以看作应该欢迎的机遇。既然变化不可避免,我们所能做的就是处理好依赖关系,将变化造成的影响的波及范围尽量减小。
19、下面总结一下“面向对象设计5大原则”和良性依赖原则在应付变化方面的作用。单一职责原则(Single-Responsibility Principle)。“对一个类而言,应该仅有一个引起它变化的原因”。本原则是我们非常熟悉地“高内聚性原则”的引申,但是通过将“职责”极具创意地定义为“变化的原因”,使得本原则极具可操作性,尽显大师风范。同时,本原则还揭示了内聚性和耦合性是“一物两面”的关系,为了降低耦合性,基本途径就是提高内聚性;如果一个类承担的职责过多,那么这些职责就会相互依赖,一个职责的变化可能会影响另一个职责的履行。其实OOD 的实质,就是合理地进行类的职责分配。开放封闭原则(Open-Cl
20、osed Principle)。“软件实体应该是可以扩展的,但是不可修名师资料总结-精品资料欢迎下载-名师精心整理-第 6 页,共 7 页 -改”。本原则紧紧围绕变化展开,变化来临时,如果不必改动软件实体的源代码,就能扩充它的行为,那么这个软件实体的设计就是满足开放封闭原则的。如果我们预测到某种变化,或者某种变化发生了,我们应当创建抽象来隔离以后发生的同类变化。在 Java中,这种抽象指抽象基类或接口;在C+中,这种抽象是指抽象基类或纯抽象基类。当然,没有对所有情况都贴切的模型,我们必须对软件实体应该面对的变化做出选择。Liskov 替换原则(Liskov-Substitution Princ
21、iple)。“子类型必须能够替换掉它们的基类型”。本原则和开放封闭原则关系密切,正是子类型的可替换性,才使得使用基类型的模块无需修改就可扩充。Liskov 替换原则从基于契约的设计演化而来,契约通过为每个方法声明“先验条件”和“后验条件”;定义子类时,必须遵守这些“先验条件”和“后验条件”。当前,基于契约的设计发展势头正劲,对实现“软件工厂”的“组装生产”梦想是一个有力的支持。依赖倒置原则(Dependency-Inversion Principle)。“抽象不应依赖于细节,细节应该依赖于抽象”。本原则几乎就是软件设计的正本清源之道。因为人解决问题的思考过程是先抽象后具体,从笼统到细节的,所以
22、我们先生产出的势必是抽象程度比较高的实体,而后才是更加细节化的实体。于是,“细节依赖于抽象”就意味着后来的依赖于先前的,这是自然而然的重用之道。而且,抽象的实体代表着笼而统之的认识,人们总是比较容易正确认识它们,而且它们本身也是不易变的,依赖于它们是安全的。依赖倒置原则适应了人类认识过程的规律,是面向对象设计的标志所在。接口隔离原则(Interface-Segregation Principle)。“多个专用接口优于一个单一的通用接口”。本原则是单一职责原则用于接口设计的自然结果。一个接口应该保证,实现该接口的实例对象可以只呈现为单一的角色;这样,当某个客户程序的要求发生变化,而迫使接口发生改
23、变时,影响到其它客户程序的可能性最小。良性依赖原则。“不会在实际中造成危害的依赖关系,都是良性依赖”。通过分析不难发现,本原则的核心思想是“务实”,很好地揭示了极限编程(Extreme Programming)中“简单设计”和“重构”的理论基础。本原则可以帮助我们抵御“面向对象设计5大原则”以及设计模式的诱惑,以免陷入过度设计(Over-engineering)的尴尬境地,带来不必要地复杂性。参考文献:敏捷软件开发:原则、模式与实践 Robert C.Martin著 邓辉译设计模式:可重用面向对象软件的基础 Erich Gamma 等著李英军等译Java 设计:对象、UML和过程 Kirk Knoernschild著 罗英伟等译重构改善既有代码的设计(影印版)Martin Fowler作者简介:温昱,架构设计师,资深咨询顾问,松耦合空间(http:/)创办人。擅长面向对象、架构和框架设计,对设计模式、UML和软件工程有深入研究。可以通过 和作者联系。名师资料总结-精品资料欢迎下载-名师精心整理-第 7 页,共 7 页 -