遵循设计模式的设计原理

 

原文作者:Bill Venners
原文链接:Design Principles from Design Patterns
译者:文星人

摘要

【注】:文中带【?】的部分是我不太确认的部分,或者是个人对原文的理解,望大家一起讨论更正,谢过!

在这篇访谈录中,Erich Gamma,作为《设计模式》这本具有里程碑意义的书的作者之一,和 Bill Venners  就两大设计的原理进行了对话。这两个设计原理是:对接口编程而不是对实现编程;使用基于类继承的对象合成原理。

 1995年Erich Gamma出版了一本热销的书,叫着《设计模式——可复用的面向对象软件》,他从此闻名于世界的软件舞台上。这本具有里程碑意义的书通常被认为是“四人 帮”(GOF)的杰作。这本书记述了对23种常见设计问题的独特解决方法。在1998年,Erich Gamma在Java社区里和Kent Becker合作开发了名为JUnit的单元测试工具。 Gamma现在是IBM设在苏黎世的国际对象技术实验室(OTI)的一名杰出工程师,他领导着Eclipse团队,并且负责在Eclipse平台上的 Java技术研究工作。

2004.10.27,Bill Venners在加拿大温哥华召开的OOPSLA大会上采访了Erich Gamma,这篇采访稿被分期刊登在了Artima出版社的 《Java前沿》,在这篇采访稿里,Gamma讲解了他对软件设计的独特见解。

@Part 1 :怎样使用设计模式。Gamma 对如何恰当地思考和使用设计模式提出了自己的见解,这些见解有别于其他的设计模式讲解,比如GoF和亚历山大的模式语言。

@Part2 :灵活性和重用。Gamma讨论了复用的重要性、投机的风险,以及frameworkitis问题。

@Part3 :GoF书中的两个最重要的设计原理:对接口编程而不是对实现编程,基于类继承的对象合成原理。

对接口编程而不是对实现编程

Bill: 在Gof书的引言中,你提到了两个面向对象的复用原理,第一个是“对接口编程而不是对实现编程”,这个原理的真实含义是什么?为什么?

Gamma: 这是一个关于在大型的应用项 目需要格外小心的依赖关系的理论。为一个类添加一个依赖关系式很容易的,仅仅需要添加一句Import语句就行,甚至像Eclipse这样的现代开发工具 已经能代你写出这些语句。有趣的是,摆脱那些不需要的依赖是一项并不容易的重构工作,尤其是在你想在其他文件中复用时这将变得更糟,基于这个原因,在引入 依赖的时候需要更加留神。也就是说依赖于接口通常是有益的。

Bill: 为什么?

Gamma: 如果你仅仅是依赖于接口进行 编程,你可以通过执行接口来解耦,由于你的执行是可变的,所以这是一种健康的依赖关系。举个例子,在测试时,你可以使用一个轻量级的实现来代替一个庞大的 数据库的执行。幸运的是,借助于现在的重构技术,你不再需要预先设计出一个接口,在全面了解了问题之后,你可以从具体的类中抽象出一个接口,预期的接口就 被这个“抽象接口”重构了。

所以这种方法赋予了你更大的灵活性,它同时将真正有用的部分——设计和实现 (implemrnt)相分开了,这也就解除了用户和实现细节之间的耦合关系。这里还有一个问题:是否应当为此一直使用Java的接口 (interfence)。其实抽象类(abstract class)也是不错的选择,抽象类在需要引入变革的时候会带来更大的灵活性,你可以为它添加一些新的行为而不必通知用户程序。

Bill :这是怎么做到的?

Gamma :在Java里给一个接口增加 一个方法时,会影响所有使用这个接口的用户端程序,但是使用抽象类的话,就可以给这个抽象类增加一个新的方法并为它提供一个默认的实现,这样所有的客户端 代码就可以继续工作而不受其影响。所以在选择的时候需要权衡,接口提供了访问基本类的自由,抽象类则提供了增加新方法的自由。【?在一个抽象类中定义一个 接口是不太可能的,但是如果硬要尝试的话必须弄明白这个抽象类是否合适?】。

接口的改变会影响用户端代码,这一点在你的代码发布之后是无法改变的,基于此 在给一个接口增加一个方法的时候必须在另一个单独的接口做同样的事情。在Eclipse中我们建立了非常稳定的API,在我们的API中可以找到被称为 I*2的接口,像IMarkerResolution2 或者IWorkbenchPart2 ,他们分别为基本接口IMarkerResolution 和IWorkbenchPart增加方法。由于通过另外的扩展的接口完成方法的增加使得用户端代码不再受到接口变化的影响。但是,这也增加了调用者的负 担,他们在运行时需要决定某个具体的类是否需要调用这个特别的扩展了的接口。

另一个值得注意的一点是:不但要关注正在开发中的版本,还要考虑后续的版本。 这不是说设计未来的可扩展性,而仅仅要求使你开发出的产品具有更持久的生命力,并长时间地保持API的稳定性,同时你也想建立软件的持续的生命周期。这一 点是我们从一开始开发Eclipse就坚持的,Eclipse作为一个平台,我们经常想把它设计得可以持续十年或者是二十年,虽然有时候这听起来会比较吓 人。

从一开始我们就不断地往基础平台上增加新的变革,IAdaptable 接口就是其中一例,实现这个接口的类能够使用另一个接口,这就是扩展对象模式的一个典型例子。

Bill :虽然相对于以前,我们现有的技术已经非常地先进了,但是我们说持久性开发时,是指持续十年或者二十年。古埃及人也创造了持久,但他们的持久......

Gamma :几千年,对吧?但是对于Eclipse,这个持久只有十年到二十年。坦诚的讲,我并不设想一个软件考古学家可以找到一张安装了Eclipse已长达十年或者二十年的硬盘,而是希望Eclipse在十年或者二十年的时间里仍然能够激活一个积极的群体。

接口的价值

Bill :上面你提到接口非常有价值,那么他们的价值何在?为什么他们的价值要大于具体的实现?

Gamma :一个接口能提取类之间的协作,它独立于具体的实现细节,同时它定义了协作集。一旦我明白了接口,我就弄懂了系统的大体,为什么?因为一旦明白了所有的接口,我就能搞懂存在的问题库。

Bill :“问题库”是什么意思?

Gamma :方法名是什么?抽象是什么? 抽象加上方法名就定义了这个问题库。Java中的Collections 包是个很好的例子,怎样使用集合这个问题库可以从List或者Set接口中提取答案,那里有一大堆的关于这些接口的具体实现,当然如果你明白了这些关键的 接口就可以将所有的(关于Collections) 的问题一网打尽了。

Bill :关于“对接口编程而不是对实现编程”,我想我的问题核心是:在Java中存在一种特殊的类叫做接口,只要我在代码中加上interface这个关键字就构造了一个接口。但是这里有一个面向对象的接口概念,而且仿佛每个类都有这么一个面向对象的接口概念。

如果我写客户端代码并需要使用一个对象,这个对象的类存在一定的等级,在最高 的等级它非常抽象,在最低的等级它很具体。我对对接口编程的理解就是这个样子,【?在写客户端代码时,我可以创建一些违背该接口等级,例如高于该接口的等 级的对象,但并不会高出很多?】。每一个等级都应当有它对应的约定。

Gamma :说得对,新建高于对应接口等级的对象是和接口编程理论相一致的。

Bill :那么我怎样去写一个实现?

Gamma :假设我定义了一个拥有五个方 法的接口,还定义了一个能实现上述五个方法并增加了其他十个方法的实现类,如果只把这个接口发布在了API中,并且你去调用那另外的十个方法,这时就是一 个内部调用,你违规地调用了一个方法,对此我可以随时打断你的执行。【?所以这是有区别的,就像Martin Fowler说的 public 和 published之间的关系一样,某接口可以是公共(public)的,但是并不就意味着你已经公开(published)了它。?】

在Eclipse里我们使用关键字“internal”定义一个内部包,这些包包含了那些我们认为不必公开的类型,即使这个包具有public 属性,这样API使用短小好用的包名,而在内部则使用长名字。显然使用包私用化类和接口是JAVA隐藏实现的另外一种方式。

Bill :现在我明白了你的意思,public和published之间存在差别. Martin Fowler已经对这个区别进行了很好的解释。

Gamma :同时在Eclipse中,即 使我们有工具的支持,我们仍然对两者的区别订立了约定。在Eclipse 3.1中,我们为那些发布了的包定义了规约,这些规约被定义在工程的class路径上,一旦你访问了那些Eclipse定义的限制包,Java开发工具会 像报告其他编译警告一样,警告你访问了internal 类,比如当你引入一个没发布的类型时你会得到一个关于你类型的反馈。

Bill :即是说如果我向接口申请一个没有公开的类,就会被阻止,因为这是一种对实现编程的行为。

Gamma :是的,从供应商角度来说就是:我需要一些自由,有权改变实现。

什么时候考虑使用接口

Bill :关于接口,GoF的书里面包含 了许多UML类图,并且这些UML图将接口和具体实现向混合,当你看它们的时候就好像看到的是代码,对于API和具体的实现 (implementation)之间的区别并不明显。相反当你看JavaDoc的时候,你看到的是一些接口。另外一个缺乏对接口和具体实现进行区别的地 方是XP,当然是XP的代码。在以测试为驱动的开发中(使用接口)将会一直改变这些未定型的代码。那么为了得到代码的整体质量程序员什么时候可以考虑使用 接口。

Gamma :你应当对设计一个应用程序和 设计一个平台区别对待。当你设计一个平台时,你必须时刻考虑哪些东西需要在API中公布,哪些东西需要隐藏。现在的重构技术使得重命名越来越普遍,所以你 必须注意避免改变已经公布了的API,这一点已经超过了定义哪些类型应当被公布。你同样必须能够回答这样的问题:你允许客户去建立这个类型的子类型吗?如 果你允许,那就会给你带来很大的义务。在Eclipse中,我们对是否允许客户建立该类型的子类型规定得很明确,借助于 Jim des Rivières 的帮助,我们的团队有了自己的API主张,Jim的不仅帮助我们定义规约,更加重要的是他使我们的API保持了一致性。

另一方面在你设计应用程序的时候,甚至你已经有了对复杂变化的抽象,你设计关 键的抽象,然后其他代码仅仅需要去处理这些抽象,而不是特例的执行,这时你的设计就具有灵活性,即使针对你的抽象产生了新的变化,你的代码仍然能工作。 【?关于XP,据我所知,现代的重构工具允许你很容易地对已有的代码引入接口,因此很容易和XP保持一致。】

Bill :这样说来对于应用程序,它的设计思想和平台的设计是一样的,但是对于小规模的应用程序,相对于平台我更容易控制和接口相关的所有用户,如果我需要改变接口,我能很容易地更新这些用户。

Gamma :是的,应用程序的设计思想和平台是一样的,你同样希望创建一个持久的应用程序,不断变化的要求不应当影响整个应用程序,事实是你已经控制了所有的用户帮助信息。一旦你发布了代码那么就不可能再访问到所有的用户,这样你就进入了API的相关工作。

Bill :即使这些客户端是由同一个公司的不同团队写成的?

Gamma :显然也是那样的。

Bill :听起来好像考虑是否使用接口关系着工程质量的提高。要是在只用两三个人的小工程里面,是不是使用接口就不再那么重要了,因为如果在小工程里面你想改哪里就改哪里,毕竟重构工具可以帮你......

Gamma :......搞定一切。

Bill :但是对于一个100人的团队来说,他们会被分为不同的组,不同的组会承担不同的责任块。

Gamma :举个例子来说,我们分配一个 构件给一个组,要求他们开发这个构件并发布对应的API,通过API来定义依赖。我们同样反对定义友元关系,这就意味着有些构件被允许使用内部类型,它们 比其他构件拥有了更多特权。在Eclipse里,所有的构件都是平等的,例如,Java开发工具plug-ins就没有特权,它和其他的plug-ins 使用一样的API。

如果你发布了API那么你就有义务保证他们的稳定性,否则你会破坏其他构件,这样的话任何人都很难取得进展。在大的工程中拥有一个稳定的API是保证工程进展的决定性因素。

在你已定义的封闭的环境里,面对变化你拥有了更多的灵活性。例如,你可以使用 Java折旧支持使其他团队逐渐追赶上您的变更。在这样的环境下,您可以移除那些在规定时间内已经折旧的方法,在充分暴露的平台上这恐怕是不可能的,即使 是过时的方法你也不能删除,因为这样可能会破坏客户端的某个地方。

合成VS继承

Bill :在GoF的书中你提到另外一个很重要的面向对象设计理论:“对象合成(composition)优先于类继承(inheritance)。”这句话的确切意思是什么?为什么这样说?

Gamma :十年后我依然认为:继承是改 变行为的一个非常酷的方法,但是继承也用它的脆弱性,因为当子类继承的方法被调用时,它很容易就能知道(父类与子类)之间的联系。父类与子类之间存在一个 很强的耦合性,由于这种耦合性使得我在子类中加入的代码会被访问到。而合成具有更好的特性,通过建立一些用于插入大尺度对象中的小尺度对象,然后由大尺度 的对象来调用小尺度对象就可以降低耦合性。从API的角度,相对于定义一个用于调用的方法,定义一个用于继承的方法将使它负有更大的义务。

当调用子类中继承自父类中的方法时,很容易就能了解到父类的内部状态,做到这 一点只需要在子类中加入一些代码。这就是为什么你应当选择使用合成的原因。一个很普遍的错误的观点是:合成根本就没有用到继承。实际上,合成使用了继承, 但是你只是实现了一个小的接口,而不是继承了一个大类。Java中的Listener就是使用合成的很好的例子,所有的Listener类都实现了 Listener这个接口或者继承了一个适配器(adapter),你创建了一个Listener类并用一个Button组件来登记它,这样Button 子类就不必为事件作出反应。

Bill :当我在设计研讨会上谈论关于 GoF的书时,我强调得最多是使用基于接口继承的合成,这有很多不同的原因。我的意思是,通过接口继承,举个例子,c++中从虚基类继承,Java代码中 从接口(interference)继承。以我提及的Listener为例子,我用MyMouseListener来实现MouseListener ,当通过addMouseListener给JPanel 添加一个MyMouseListener得实例时,就使用了合成,因为JPanel拥有 MyMouseListener的实例,这样就能调用它的mouseClicked 方法了。

Gamma :是的,这就降低了耦合性,另外也得到了一个独立的listener对象,你不能将它和其他的任何对象相联系。

Bill :我已经注意到了相对于继承,合成具有额外的灵活性,但是我很难讲这一点讲清楚,也是我想让你表述的地方。为什么会是这样?实际上是怎样实现的?增加的灵活性究竟是来自哪里?

Gamma :我们把这一点称着黑盒复用。 想象你有一个容器,接着你放一些更小的对象进去,如果容器委托了一些行为给这些更小的对象,这些更小的对象是可以设置并定义了这个容器的行为,最后你通过 更小的对象的设置定义了容器的行为。这一点带给你灵活性的同时,也使你有重用更小对象的机会,这是非常强大的。比起给你大段的解释,我用策略模式来进行说 明,策略模式是合成比继承更具灵活的最典型的例子。曾加的灵活性来自于你可以插入不同的策略对象,并且你可以在运行时动态地改变策略对象。

Bill :那么如果我想使用继承......

Gamma :你不能将那些策略对象很好地搭配使用,特别是你不能在运行时动态地改变他们。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值