《编程机制探析》第七章 设计模式

第七章 设计模式

什么是真正的面向对象的设计?这是一个困扰我多年的问题。
当年,面向对象的各种神话甚嚣尘上,一个程序员要是不能侃上两句面上对象,都不敢出门见人。
那时候,我接触的第一门面向对象语言是C++。那是一门极其庞杂的语言。语法繁复不说,更令人头痛的是,C++语言还有各种变种,即使是同一种变种,其编译器的实现也有可能不同,存在着大量的编程陷阱。那些陷阱主要集中在C++语言的多继承语法特性上。C++允许一个类从两个甚至更多的类继承。这使得一个多继承类的内存映射结构极其复杂,各种编译器的实现也莫衷一是。
幸好,我那时候已经学了汇编,对于各种类结构的内存映射已经有了粗浅了解。理解多继承内存结构,对我来说,已经不存在难以逾越的理解难度,只剩下繁琐的细节考究。我费了九牛二虎之力,终于把C++这块难懂的骨头啃了下来(至少,我当时是这么自以为的)。
然后,我就彷徨了。下面,我该学习什么?我把目光投向了STL(Standard Template Library)。
STL对于C++程序员来说,是耳闻能详的一套模板库(Template Library)。STL这套模板库中既实现了一部分C++语言的特殊问题,比如,对象引用自动计数和内存自动释放;也解决了一些通用问题,比如,集合类型的遍历、统计、筛选等普遍的算法问题。
如果你不用C++,那你不用了解STL。那些针对C++语言的特殊问题,例如内存自动释放,你也接触不到。而那些关于集合类型的通用算法,在其他语言中也都有对应的功能。
STL是基于C++ Template技术实现的。C++ Template是一种极为强大的预编译技术,其工作原理为在编译期间将Template定义的代码展开成真正的C++代码。C++ Template之强大,远远超过了你我的想象。STL只不过是它的一个牛刀小试。
微软公司的一帮C++牛人,后来又开发出WTL(Window Template Library)和ATL(Active Template Library)。WTL是一套轻巧的Window窗口编程模板库,旨在替换笨重的窗口程序开发库MFC。ATL是一套轻巧的COM组件开发模板库。
我不得不承认,我被WTL和ATL这两个模板库打败了。我看不懂这两套模板库。我这人有个毛病,不了解机理的东西用起来总是心头惴惴。
STL我勉强看得懂,我还因此而沾沾自喜,打算在C++领域中继续进军。但是,ATL和WTL这两个东西的出现,如同当头给我泼了一盆冷水,打消了我这个不切实际的妄想。我那点可怜的微不足道的脑细胞,不值得消耗在那些C++ Template设计者构造出来的种种匪夷所思的奇思妙想中。
我转向了一个不那么难的方向——面向对象设计。虽然这个名词在我听起来是那么的虚,但是,既然有这么多人在这个领域中吹水,那就说明,这个领域是大有油水可捞的。说不定,我也能浑水摸鱼呢。
但是,什么才是真正的面向对象设计呢?我的心里发虚。我不能回答这个问题。我进行过很多种思索和尝试,但我没有找到答案。我心里没底,我直不起腰杆来,我不敢说,我掌握了面向对象,我只能说,我只掌握了一点面向对象语言C++语法的一点皮毛。
那时候,面向对象代码的实现手段主要有三种:继承,包含,参数。
继承,就是一个类继承另一个类,从而实现对父类的重用。
包含,就是一个类,包含另一个类对象作为成员,然后,应用被包含类的方法,从而实现对被包含类的重用。
参数,就是一个类的方法,接受另一个类对象作为参数,然后在类方法中调用作为参数的对象的方法,从而实现对被调用的类的重用。
其中,继承这种方法是被人所诟病的。继承实现的重用,实际上是一种静态重用,父类已经固定了,无法实现面向对象中最重要的特征——多态。
前面讲了,只有在框架重用(Framework Reuse)这种重用模式中,才能发挥出多态的优势。而框架重用,主要就是采取后两种方式——包含和参数——来实现的。本质上来说,包含和参数,这两种设计方法是一致的。我们可以通过setter方法来更换内部成员对象的实现,其效果等同于同更换参数对象的实现。
这就是我学到的关于面向对象设计的全部知识。那时,我心里很虚,不断地扪心自问,难道,这就是面向对象设计吗?这只不过是在C语言回调函数的基础上前进了一小步而已。假如这就是面向对象的话,那么,建筑在面向对象概念上的程序设计大厦,简直就是建立在流沙上的危楼,总有一天会被人戳穿,轰然倒塌。为这样一个华而不实的东西浪费生命,到底值不值得?
对于某些人来说,所谓工作、事业,只不过是一种谋生赚钱的手段,管它是华而不实还是实而不华,只要能赚到钱就行。但我是一个幼稚的人。我做事之前,总要反复地诘问自己,这样做的意义何在?对我有什么意义?对他人有什么意义?对社会有什么意义?
大家都知道,思考人生的意义,这是一个多么宏大、又多么无稽的主题。千百年来,多少圣贤都不见得能解决这个问题。我可怜的头脑更是无法承担起这样的重任。理所当然的,我陷入了心理危机。我就像一个歧路彷徨的旅人,望着远处亦真亦幻的繁荣景象,却不知道那是真实的都市,还是海市蜃楼,不敢举步向前。
这时候,设计模式(Design Pattern)出现了。或者说,设计模式终于流行起来了,以至于我这样的总是落后于时代的人也听说到了。
设计模式的出现,如同在平静的水面中投入了一块石头,顿时在程序设计界激起了一片片涟漪(如果不是波澜的话)。
设计模式(Design Pattern)的概念最先是四个牛人提出来的。人们戏称他们为“四人帮”(Gang of Four)。在我的印象中,这个称呼好像最先是那四个人对自己的自称。这个问题且不去管他,我们来深究一下设计模式的概念。
设计模式是程序员过往设计经验的总结,是程序员针对某一类通用问题总结出来的通用设计方案。
比如,设计模式书籍里面最常出现的例子——观察者模式(Observer Pattern)。这个模式有很多其他的别名,例如,监听者模式(Listener Pattern),订阅者模式(Subscriber Pattern),或者发布者模式(Publisher Pattern)。
不管叫什么名字,它们都是指同一种设计模式。我更喜欢订阅者模式(Subscriber Pattern)或者发布者模式(Publisher Pattern)这两种说法。因为,在实际的应用中,我们能找到应用实例——新闻订阅。
我们可以在一些新闻中心注册自己的邮箱(电子邮箱或者真正存在于门口的收件箱)或者手机号码,然后,那些新闻中心就会定期地将一些新闻发送到我们的邮箱或者手机里。
在这个模型中,新闻中心就是一个发布者(Publisher),其内部维护着一个订阅用户列表(Subscriber List)。每当需要发布新闻的时候,新闻中心就会根据订阅用户列表,将新闻一一发送到每个订阅用户的手中。
其具体实现逻辑为,每个订阅用户都给新闻中心提供了一个接受新闻的接口方法(Interface Method),新闻中心遍历订阅用户列表的时候,就可以一一调用每个订阅用户的接受新闻的接口方法,就可以将新闻推送到用户的手中。
这个设计模式在诸多设计模式书籍中都有详细的阐述,本书就不给出具体代码了。
其余的设计模式,也都有各自针对的一类通用问题。本书后面会重点讲解一些极为典型、极为常见的设计模式,这里就不一一列举了。
设计模式的思想,对于中国人来说,尤其是对于受过传统教育的中国人来说,不难理解。因为,中国从古至今,就有用典的习惯——即,借古喻今,借用古代典故,指代现在的类似事件。设计模式也是这样,借用以前曾经出现过的程序设计场景,来指代现在或者将来会出现的同一类程序设计场景。
我第一次接触到设计模式的时候,立刻就被其朴实易行的思想深深西引住了。那是一种如获至宝的感觉。我激动得不能自已。原来,程序竟然还可以这样设计。
我至今仍然记得那种强烈的感觉。设计模式的出现,如同黑暗中的一道闪电,黎明前的一道曙光,瞬间就划开了夜空,给我指明了前进的方向。
有人说过这么一句话,“只有理解了设计模式,才算是真正懂得了面向对象设计。”
此话,我是深以为然。我不知道别人怎么样,但对于我来说,确实就是如此。只有在接触到了设计模式之后,我才算真正触及到了一点面向对象设计的边缘。
设计模式的出现,就像给我吃了一颗定心丸。我突然发觉,原来,面向对象设计并不是一个包装在一个浅显基础上的巨大泡沫,而是有些真东西的。这些真东西就是设计模式。可以毫不夸张地说,设计模式构成了面向对象设计大厦的坚实骨架。
这是不是说,非面向对象的语言就无法用到设计模式了呢?非也。设计模式是通用的设计惯例。非面向对象的语言同样可以借鉴。只是说,面向对象语言应用起设计模式来,尤为顺手而已。因为,各种各样的设计模式,究其根底,全都是框架重用(Framework Reuse),即,不变的是框架,重用的也是框架,变化的是各种具体类的实现。
更进一步说,设计模式中不变的是对象之间的固定关系。对于设计模式来说,对象之间的固定关系,就是重用的框架。即,设计模式重用的,就是对象之间的固定关系。
那么,对象之间的固定关系如何来表达呢?第一个方法,当然是用参数。比如,前面章节中所举的框架重用的例子。我们可以在类的对象方法的参数列表中包含另一个类对象作为参数。这样,我们就建立了这两个类之间的框架(Framework)和回调(Callback)关系。
但是,这种“参数设计法”的表达能力毕竟是有限的。比如,前面举的那个订阅者模式(Subscriber Pattern)的例子。在这个设计模式中,发布者(Publisher)需要维护一个订阅者的列表。在订阅者注册的过程中,发布者需要将订阅者加到列表中。当发布者发布新闻的时候,发布者还需要遍历列表,将新闻发送到每个订阅者的手中。
在这个模型中,订阅者列表用成员变量(即“包含设计法”)来表达,显然再合适不过了。如果我们单纯用“参数设计法”来表达的话,参数将会十分复杂,代码将会十分繁琐。有兴趣的读者可以自己尝试一下——假如什么成员变量都不用的话,应该如何实现订阅者模式?
可以这么说,在设计模式中,对象之间的固定关系,绝大部分都是依靠成员变量互联在一起的。在一些框架应用中,对象之间的关系甚至复杂到这样一种程度,需要用对象图(Object Graph)或者对象网络(Object Network)来表达。
近几年,有一种叫做“IoC Container”的程序设计方式悄然兴起,蔚然成风。IoC是Inversion of Control的缩写,字面意思是“反转控制”。至于其真正意思,我也懒得解释。IoC的概念相当出色和先进,但是,这个词本身却创造得十分失败。每次一提起这个词,我都感觉说不出的别扭。说的人难受,听的人也难受。可见,一个好的概念,不能缺少一个好的名字。
有一个词,叫做Dependency Injection(依赖注入),表达和IoC同样的意思。这个词虽然也不怎样,但至少意思上还说得通一些。不过,这个词的缩写DJ很容易同某种酒吧歌舞主持人之类的职业弄混,不如IoC这个缩写那么独特。而且,原创者使用的就是IoC这个词。出于对原创者的尊重,人们也更多地使用IoC这个词。
我这里就直接解释IoC Container整个词的含义。Container的意思是容器,这个容易理解。而且,这个词,用在这里,也是最恰当不过了。IoC Container是一种用来组装复杂对象关系——即对象图(Object Graph)或者对象网络(Object Network)的容器。应用这个容器,程序员可以灵活地将自己实现的对象组装起来,构成一个复杂的对象关系网络。这个目标,恰好与设计模式的目标重合。我们甚至可以这么说,IoC Container是一种极为通用化的设计模式。只是IoC Container这种设计模式太过通用化了,我们几乎总结不出什么值得一说的设计规律。
我对于IoC Container的看法是这样的。首先,IoC Container的概念相当先进,相当美好,很值得研究。因此,我强烈推荐读者去自己搜索、学习并理解Ioc Container的理念和思想。其次,我对于现有IoC Container的实现方案和应用方式都不是很满意,因此,我在本书中并不想写关于IoC Container的具体例子。
IoC Container是对象关系的极端表现,设计模式涉及到的对象关系远没有到那么复杂的程度。有些设计模式的对象关系极为简单,用简单的“参数设计法”就足以应对了。对象关系复杂一点的设计模式,就需要动用“包含设计法”(即用成员变量来关联其他类的对象)了。这时候,面向对象语言的优势就一目了然、舍我其谁了。非面向对象语言很难在对象关系组织复杂度这方面与面向对象语言竞争。
表达复杂的对象关系,并把对象的具体实现从这种复杂的对象关系中剥离出来,正是面向对象语言和面向对象设计的真正的精髓所在。
理解了这一点之后,我们就可以真正进入面向对象设计——即设计模式——世界了。首先,我们从对象关系最简单的设计模式——访问者模式(Visitor Pattern)——开始。
访问者模式(Visitor Pattern)这个名字起得并不好。但是,我也不能为这个设计模式找出一个更好的名字来,姑且就这么从众用着。
之所以说Visitor Pattern这个设计模式简单,是因为Visitor Pattern表达的就是最简单的“框架(Framework)+ 回调(Callback)”重用模式。其组成可以分为两个部分。第一部分,是一个过程(或者叫函数,或者方法),这个过程承担着Framework的角色,接受一个Visitor对象作为参数;第二部分,自然就是Visitor对象,这个Visitor对象,自然就承担着Callback的角色。
从上述描述,我们可以看到,Visitor模式确实是最基础的设计模式,表达了最基本的框架重用模式。
当然,这只是广义上的Visitor Pattern的含义。狭义上的Visitor Pattern有可能代表各种更具体的设计惯例。比如,Functor Pattern(函数子模式)就是Visitor Pattern的一种简单变体。
Functor的概念最先出现于C++ STL的集合算法中,比如,遍历、求和、排序等。这些算法自然就是可重用的框架(Framework)部分,而这些算法所接受的参数Functor,自然就是Callback的角色。
设计模式的最终目的也是为了重用,而且是设计上的重用。既然是重用,自然是重用那些不变的部分,即框架(Framework)部分;那些变化的部分,自然就是那些需要从设计模式中被剥离出去的具体实现(即表现出多态性的Callback部分)。
下面,我们就以排序算法为例,一步步将排序这个通用问题中的不变部分和变化部分分离出来。最终剥离出来的不变部分,就构成了设计模式。通过这个过程,我们能够更好地理解设计模式到底是怎样一步步产生出来的。
我将使用Java语言来书写后面的例子。为什么要选择Java语言来描述设计模式呢?
第一,我使用这门语言的时间最长,因此也最熟。
第二,至少到目前为止,Java语言还是最流行、最热门的语言,使用者最多。
第三,Java语言和C、C++、C#语法比较相近,而这三门语言的使用者也是相当多的。
第四,Java语言中高调而鲜明地规定了interface(接口)和class(类)两种类型,从语法概念上就把定义和实现部分清晰地分开了。而设计模式作为面向对象设计中的骨干,所要达到的一个主要目标之一,就是定义和实现相分离。因此,Java语言尽管不如Python、Ruby等动态语言那么灵活强大,但是,用来描述设计模式,却是相当合适的。
同前面的原则一样,本书不负责讲解Java语法的入门知识,那是其他书籍的任务,或者是读者自己的任务。现在,让我们直接跳入到Java代码中。
假设一个名为Record的Java类,有field1, field2, field3三个成员变量。
public class Record{
public int field1;
public long field2;
public double filed3;
};
在一些老究似的面向对象专家看来,上述的类设计完全违反了面向对象的原则——所有的成员变量全是公开的,严重破坏了封装性。
没错,按照所谓的面向对象原则,所有的成员变量都不应该公开,而是应该封装起来,并暴露出来一组setter和getter等访问方法。据说,那样可以更好地封装变化,从而更好地应对以后的代码变化。可能这种说法是对的。但我没有闲工夫去写那么多代码。而且,读者也不愿意见到那么多假单重复的代码占据太多的篇幅。所以,我这里明确地声明,我这里的Record类就是当做C语言的中structure来用的。里面只有数据,不包含任何方法,也不需要任何可能的扩展和修改。
好了,闲话少说,言归正传。我们现在有一个Record对象的数组Record[] records,我们需要对这个records数组按照不同的条件进行排序。
首先,按照field1的值从小到大进行升序排序。排序代码如下:
void sort(Record[] records){
for(int i =….){
for(int j=….){
if(records[i].field1 > records[j].field1)
// 交换 records[i] 和 records[j]的位置
}
}
}
这个排序算法没什么好说的,只是一个最简单、最直接的排序算法。任何一本讲解数据结构和算法的入门书籍都会讲到。关于排序算法这部分,本书不会做更多的讲解。放在这里的代码只是一个示意代码,表示这里的排序算法是固定的、不变的框架(Framework)部分。
现在我们换一个排序条件,按照field2的值从小到大进行升序排序。
void sort(Record[] records){
for(int i =….){
for(int j=….){
if(records[i].field2 > records[j].field2)
// 交换 records[i] 和 records[j]的位置
}
}
}
如果我们想按照field3的值从小到大进行升序排序,同样可以写出类似的代码。我们可以看到,整个算法部分是不变的,变化的只有判断条件部分(即if判断语句中包含的红色字体的那部分代码)。我们可以引入一个Comparator接口,把这个变化的部分抽取出来。
public interface Comparator(){
public boolean greaterThan(Record a, Record b); };
然后,我们把判断条件抽离出来,作为回调参数。sort过程就可以这样写。
void sort(Record[] records, Comparator comp){
for(int i =….){
for(int j=….){
if(comp.greaterThan(records[i], records[j]))
// 交换 records[i] 和 records[j]的位置
}
}
}
对应第一个要求——对records数组按照field1的大小排序,我们可以写一个实现Comparator接口的CompareByField1类。
public class CompareByField1 implements Comparator{
public boolean greaterThan(Record a, Record b){
if(a.filed1 > b.filed1)
return ture;
else
return false;
}
}
我们只需这样调用sort过程:sort(records, new CompareByField1())
同样,对应第二个要求——对records数组按照field1的大小排序,我们可以写一个实现Comparator接口的CompareByField2类。
public class CompareByField2 implements Comparator{
public boolean greaterThan(Record a, Record b){
if(a.filed2 > b.filed2)
return ture;
else
return false;
}
}
我们只需这样调用sort过程:sort(records, new CompareByField2())
这就是一个最简单的Visitor Pattern。其中,sort是可重用的算法框架(Framework)部分,而Comparator则是抽离出来的变化部分,即callback部分。在Visitor Pattern中,Comparator就承担了Visitor(或者叫Functor)的角色。
在Java语言最基础的标准开发包中,有一个叫做java.util.Collections的类。这个类里面定义了一堆与集合操作和计算相关的静态过程方法(Static Method),其功能等同于STL里面的集合操作算法。
Collections类中定义了一个sort过程,该方法接受一个java.util.Comparator接口类型的对象作为参数。
本章前面给出的例子,就是仿照Java语言开发包(Java Development Package,简写为JDK)中的sort过程和Comparator接口写出来的。当然,本章给出的例子做了简化,更加浅显易懂。比如,本章的sort算法极其简单,比Java语言开发包中sort算法要简单得多。另外,Java语言开发包中sort过程接受List类型作为集合数据参数,而本书为了简化起见,直接使用了Record[]数组类型作为参数。有兴趣的读者,可以用Java开发包中的sort过程和Comparator接口来实现本章中的例子。
另外,需要特别提出的一点是,本章中讲述的Visitor Pattern,是最简单的一种Visitor Pattern,集合里面的数据只有一种类型。
在一些复杂的情况下,集合里面的数据类型可能有多种。这时候,Visitor的接口方法中就需要针对集合元素的不同数据类型定义不同的接口方法。这种针对不同数据类型定义不同方法的代码写法,有一个名字,叫做Type Dispatch(类型分派)。
在Visitor Pattern中,Visitor本身是多态的。比如,上面例子中的Comparator可能有不同的具体实现。从这个意义上来说,这也算是一种Type Dispatch,而且是动态的运行期的Type Dispatch。因此,这种集合元素类型多样化的Visitor Pattern也叫做Double Dispatch(双重分配)。
说句心里话,我很不喜欢这种Double Dispatch的Visitor Pattern。因为,在Visitor接口上根据不同参数类型定义一大堆类似的接口方法,极为烦冗琐碎,不仅没有发挥面向对象设计的多态特性,还完全破坏了面向对象设计的美感。在我个人看来,这种Double Dispatch的Visitor Pattern不是一种好的设计模式,本书也不会对这种Visitor Pattern进行阐述。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值