初学设计模式的一点感悟

最近几个星期的时间对设计模式做了入门学习,之前读过GoF的那本《设计模式》,感觉非常晦涩。在面向对象开发技术这门课中有设计模式的内容,这才重新拾起这个东西,发现比之前那次学明显容易多了。自己反思了一下,应该是和后来打好了比较好的面向对象思想的基础有关,所以也建议想要学习设计模式的朋友,先把面向对象的基本思想搞明白,起码得知道多态是干嘛用的,为什么要多态,多态有什么好处,这些问题可以看我之前写的那篇《浅谈多态机制的意义及实现》。然后再来学习设计模式,才算有迹可循。

本文是对设计模式的入门学习做一些感悟和分享,希望能和大家一起多多交流关于模式的心得。

一、基本概念

1、被封装的应该是什么

通常意义上,我们习惯于认为,面向对象最大的特点就像UML图一样,是由错综复杂的继承和依赖关系组成的。但是Alan Shalloway认为,对象真正的威力并不在于继承,而是来自封装行为。

封装是面向对象三大特性之一,一般我们会认为,封装就是对类里面的实例变量进行保护,使其对外部类不可见。我们似乎很少会去关注对方法和行为的封装。就像我们经常做的:把实例变量设为private,把方法设为public,对于实例变量,我们会编写一系列set/get方法来间接修改/获得。

但是问题并不仅仅到此为止。尽管我们把数据隐藏了,但是通过set/get来操作数据,总让人感觉有一种换汤不换药的味道。一种更为深刻的思想是:封装不仅仅是“数据隐藏”,封装应该被视为“任何形式的隐藏”。除了数据我们还可以隐藏以下各种东西:实现细节、派生类、设计细节、实例化规则。

最简单的就是我们可以在重构代码时把某些类内部使用的方法标记为private,因为这些方法仅仅是这个类内部使用,并不想给外部类调用。另外一方面,当我们使用抽象类或是接口时,事实上就已经默许了派生和实现的封装:你不会知道这个类将会有哪些子类去继承它。而精髓恰恰在此:通过多态实现的类型封装比单纯的数据封装有更深刻的意义。

2、视角框架

Martin Fowler提出了面向对象范型应该考虑的视角。他建议面向对象程序员应该从三个视角去考虑问题:

1)概念视角:软件是用来做什么的;

2)规约视角:提供了哪些接口(如何调用方法去完成这个任务);

3)实现视角:这些接口/方法该怎么实现。

在设计时,我们通常是从概念和规约的视角去看待问题:首先明确软件的目的(概念视角),然后通过设计一系列接口以及接口提供的方法来对程序整体作架构设计(规约视角)。这个阶段我们并不应该过多考虑实现的细节,因为实现相比设计总是容易的。但是初学者通常会在设计时不由自主地陷入实现的泥潭中:这个设计在代码层面该如何实现?用什么API?究其根本我认为正是因为实现相比设计更具体,这样的问题更容易解决的缘故。

而在具体实现时,就应该从规约和实现视角去解决问题:根据接口设计的方法(规约视角),通过实现类的代码做具体实现(实现视角),此时才应该考虑之前的问题。

3、对象究竟是什么

传统上对象被视为具有方法的数据,很显然,对象是类的实例,在类这个蓝图中,包含了对象可以拥有的数据变量和方法(仔细想一想你之前对对象的认识是不是和这种观点有类似之处)。

当我们明确了封装更深刻的意义在于对行为的封装时,自然而然地,我们可以对对象作出更大的期待:对象不仅仅是一个包裹,它应该像一个人一样,去完成一些事情。所以,对对象更有意义的定义应该是:对象是具有责任的一个实体。这样我们就明确了对象的用处,同时也能让人更加关注对象的责任,关注对象究竟该干什么而不是拥有什么。不明确这一点,对象职责的解耦就无从谈起。

另一方面,如果我们采用Martin Fowler的视角框架来观察对象,我们就可以从三个视角来对对象作更全面的认识:

1)在概念层次上,对象是一组责任。

2)在规约层次上,对象是一组可以被其他对象或对象自己调用的方法(也称行为)。

3)在实现层次上,对象是代码和数据,以及它们之间的计算交互。

看,之前提到的那种传统的看法仅仅只是停留在实现层次上。

二、设计方法

1、设计步骤

Alan Shalloway认为,传统的从需求分析中找出名词作为类,找出动词作为方法的设计思想是一种非常有局限性的方式,缺点之一就是会产生大量原本可能并不需要的类。

在这里我们引入Jim Coplien的共性分析和可变性分析的概念。

共性分析就是寻找一些共同的要素,这些要素可以帮助我们理解成员之间的共性在哪里。

可变性分析建立在共性分析的基础之上,它要求找出元素之间的差异所在。

具体来说,共性分析帮助我们提取出抽象类或接口作为规范的定义存在,而可变性分析则帮助我们针对这些接口进行差异化的实现。

明确这一个问题的关键目的在于,我们需要为类组织起结构,而不仅仅是类与类之间单纯的消息传递。

从架构的视角来看,共性分析为架构提供长效的要素(正如我们所知道的,接口通常不会变化),而可变性分析则促进它适应实际使用所需(不同的实现方式)。共通的概念用抽象类或者接口表示,可变性分析所发现的变化通过具体类实现。

2、组合优先于继承的更多优点

设计模式中提倡组合优先原则,当然这是针对继承的许多缺点所做的对策。我们不妨思考一下,继承最大的优点在哪?毫无疑问,是代码复用,面向对象出现之初的一个口号就是复用代码。但是如果继承给我们带来的好处大于其坏处,那在继承时或许我们应该更保守一些。事实上,重用并不是使用面向对象方法的根本原因。降低维护成本和使代码更加灵活、更容易扩展才是更重要的考虑因素。为什么我们要设计?目的就在于此。使用正确的面向对象技术当然可能实现重用,但并不是通过直接使用该对象,然后派生新的变体对其重用即可达到的。这样只会产生难以维护的代码。(想想Bridge模式的初衷吧!)

3、整体和部分的关系

在谈到整体与部分的关系时,我们必须介绍一本著作,Christopher Alexander的《建筑的永恒之道(The Timeless Way of Building)》。这本书本来是讲建筑的,但其最受追捧的领域却是在软件界。它对模式的探讨带给软件开发人员许多思考,软件开发是否也像建筑一样,存在某些公式化的模式,只要我们直接去套用,就能像建高楼一样。又快又好地开发出理想的软件?

首先对这个问题的解答让人失望,直到目前为止,并不存在这样一种套路可以保证软件的质量。1986年,Fred Brooks发表了一篇著名的论文《No Silver Bullet — Essence and Accidents of Software Engineering》,“没有银弹”是许多人的遗憾,也成为软件工程师们的一个梦想。有兴许的朋友可以去读Scott Rosenberg的《梦断代码(Dreaming in Code)》,这本书讲述了两打优秀的程序员在软件开发的过程中的辛酸故事,让人感慨。

《建筑的永恒之道》一书中有这么一段话:每个部分都因其存在于更大整体的背景中而被赋予了特定的形式。这是一个分化的过程。它把设计看成是一系列复杂化的活动;结构是通过对整体操作、使其起皱而注入其中的,而不是通过一小部分一小部分添加而成。在分化的过程中,整体孕育了部分:整体的形式及其各个部分是同时产生的。分化过程就好像是胚胎的成长过程。

通常我们在设计软件时都习惯于自底向上设计:先把最底层的工具类实现好,再使用这些工具类实现一些略微高层的类,再通过这些高层的类去组合成更上层的类。这似乎是理所当然的:没有下面的东西,上面的抽象怎么可能正常工作?Alexander却认为,无论是设计什么,都应该从整体入手,在对整体的不断细化中再考虑具体的部分。

令人感到微微诧异的是,我们在生活中正是这么做的。假设现在你想要一辆新的自行车,那么也许你会这么去描述它:首先这是一辆自行车而不是摩托车,然后它的整体颜色应该是红色,把手应该是向下弯曲的,有一个车铃在右侧把手上,后轮的中间有变速器,变速器可以调整4道变速……

这不正是从整体到部分的层层细化吗?这正是Alexander所认为的正确的设计之道。

从这里我们不难得出一个结论:如果我们实现从部分的角度开始考虑设计,最终将预先形成的部分(模块)组合起来,我们便无法得到一个优秀的软件设计。

4、设计模式的用法

首先,设计模式不是万能的,更不是公式化的,并不存在某一个问题必须套用某种模式的说法。模式应该相互配合,共同解决问题,这才是设计模式的正确用法。

千万不能生搬硬套模式,我个人比较倾向于去深刻体会并理解模式的思想,而不是把模式的名字都记下来,再分别细述。

模式的理想使用我认为应该是这样,举例来说,当我要使用一个旧的类而不能直接使用时,我的脑海里应该自然而然地冒出来在中间加个转换器的想法。当然,这里我很清楚,这个转换器就是适配器。而当我要对一个系统作调用的简化处理时,应该非常自然地冒出来封装入口的思想。注意,在这里我自始至终没有刻意提起Adapter或是Facade模式,名字神马的都是浮云,理解它们的原理和作用才是重点。设计模式是被人发现和总结出来的,而不是人为发明的,没有设计模式的时候也有人这么做,我们的思想应该向他们靠拢。

而在进行设计决策时,我们通常会面临好几种不同的解决方案,它们各有利弊,该如何选择?许多开发人员会这样问:“这些实现方式中哪个更好?”这种问法并不明智。问题在于,往往没有哪个实现方式天生就优于另一个(否则另一个还有存在的意义吗?)。应该这样问,对于每种实现方式,“什么情况下它优于其它方式?”然后再问:“哪种情况与我的问题领域最相似?”在这种思考中,我们会逐渐对不同的解决方案和模式有更深刻的体会和认识。

三、常用模式的心得体会

这里对集中常用的设计模式作一些感悟总结,我喜欢对比模式之间的区别,以此对模式的适用领域和根本思想做深入的分析并理解,但还是要强调,不能把模式孤立看待,必须让模式互相配合,共同解决问题。

1、Facade/Adapter/Decorator的联系和区别

曾经看到有人把Facade和Adapter两种模式进行了对比,因为它们都存在对类的包装(wrap),事实上我认为Adapter和Decorator更像。Adapter完全可以理解为是对不符合新接口的旧对象进行装饰,区别在于在Decorator里,被装饰的对象也继承于装饰者的父类,而Adapter里被适配的对象不行(正是因为不行才需要适配)。同时,Decorator中的父类的作用更侧重于是便于对类的组织。

明确三种模式的目的有助于我们更精确的理解其思想。

Facade:简化接口,避免复杂逻辑和调用;

Adapter:转换接口,相当于两相插座和三相插头之间的拖线板;

Decorator:增加额外特性。

2、Facade/Proxy的区别

Facade通常使用多种对象组合实现功能,但Proxy通常只使用一类对象。此外Proxy的接口方法通常与被调用的对象方法名相同,以此保证调用代理和调用实际对象的方式一致。

Facade通常用于简化系统的使用(典型例子就是API)。此外它还有如下两个作用:

1)通过限定使用入口来对系统进行监测。

2)对系统的修改可以对调用者透明,便于系统切换和更新。

Proxy的典型用途是:

1)简化对象调用(比如权限控制之类的事情可以在Proxy内部实现,不需要调用者操心)。

2)使对象调用轻量化,原本需要对N个对象进行调用的操作,现在只需通过一个Proxy,内存开销大大减小。

3、有switch,考虑Strategy模式

switch语句常常说明:

1)需要多态行为。

2)存在指责错放(类的职责过多)。

这里可以采用Strategy模式配合多态简化判断逻辑,把职责下放给子类,消除集中的父类职责。

4、Bridge模式

Bridge模式是典型的“组合优先于继承”的范例,牢记一点:对象和方法是多对多关系的时候,就可以考虑使用Bridge模式,而不是滥用继承。此外Bridge模式对系统的可扩展性有极大的帮助。

5、Template Method模式

我觉得Template Method模式和重构中的Extract Method非常相似,都是提取公共代码从而消除冗余并避免重复代码潜在的隐患。而Template Method模式最常用的的意图是:定义一个操作中算法的骨架,而将一些步骤延迟到子类中。在不改变算法的结构的前提下重定义它的步骤。这种思想和之前讲的反过来,先有父类,定义算法骨架,其中某些步骤是确定的,某些是不确定的或是需要变化的。根据可变性分析,后者就应该延迟到子类实现,子类实现时只需要实现那些会变化的方法即可。

初学设计模式的一点感悟》上有2条评论

发表评论

电子邮件地址不会被公开。