设计模式沉思录(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-18 21:23:39

点击下载

作者:[美]约翰·威利斯迪斯(John Vlissides)

出版社:信息技术第一出版分社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

设计模式沉思录

设计模式沉思录试读:

前言

,显然他认为对那些潜藏在自然中的事物,我们应该去挖掘和发现它们,而不是关注创造它们的“方法”,甚或是从其璀璨的裂缝中窥探它们。

能够与GoF(模式四人组)之一共同写专栏,我不仅感到高兴,更感到荣幸。如果没有 GoF 的《设计模式》一书,读者也许都不曾听说过模式。通过对模式的介绍,该书成为了这个全新科目的极佳教材。GoF的23个设计模式奠定了一个不大但却非凡的基础,并发展壮大成为我们今天知道的模式社区。而凭借本书读者可以直接深入了解GoF作者之一John的思考过程,同样对更加广阔的过程有一些间接的了解。

为了总结出一个好模式,突破一些局限在所难免,就像小鸡破壳而出,而John 的专栏对那些在《设计模式》背后发生的“破壳”的对话进行了探索。例如,John在类结构不断演变的情况下,对Visitor的限制进行了探索。他还谈论了一些模式,比如GENERATION GAP(见本书的第3 章)。这些模式未能被收录到《设计模式》中,但它们可能已经足够好了,值得公之于众。读者会发现GoF关于MULTICAST模式的对话,这段对话让John陷入沉思:“一旦了解了我们在模式开发过程中所经历的混乱,那些认为 GoF 具备非凡能力的人一定会感到震惊。”本书传达了一个重要的事实,它没有出现在更为学术化和更加完善的《设计模式》一书中:模式源自一群认真努力的程序员,虽然他们不可能每次一开始就把事情都做对,但他们努力地让那些重复出现的设计实践变得实用。我认为阅读本书的模式用户将会感谢 GoF 为他们的模式而付出的心血,我还认为阅读本书的模式编写者在今后发掘和编写模式时会比以前更加谦逊和勤勉。

乱中求序是自然科学的主旋律,那么,我们不应该认为设计的科学会有任何的不同之处。模式是人们在工作中共同发现一些构成人们高品质生活的因素,并将它们加以记录的整个过程。这不可避免是个有机过程。贯穿本书,读者将得以洞察各模式背后的有机过程,得以了解普通(但非常有经验而且非常尽职的)软件开发人员在努力形成自己对设计的理解时的思考过程。《设计模式》是对他们集体理解的提炼,而本书是对产生理解的过程的提炼,我们不应该低估它在解释GoF模式方面所带来的价值。请允许我引用一封我在 1997 年晚些时候收到的来自 Richard Helm 的信,我相信它进一步证实了这一点。

GoF 的设计模式只解决了微观架构(micro-architecture)。你仍然必须把宏观架构(macro-architecture)设计好:分层、分布、功能隔离……。而且就像 Cope 说的,你仍然必须把纳米架构(nano-architecture)设计好:封装、Liskov……。在所有这些中的某个地方,你也许会用到一个模式,也许用不上。即使用上了,也可能和某本书中的介绍和描述很不一样。

这本书将帮助你理解如何将《设计模式》——其实是任何关于设计模式的书籍——当作一本珍贵的指南,而不是当作一些累赘的规定。它可以帮助你在更广阔的面向对象设计的基本原则下,将设计模式运用到合适的地方。它道出了虽然不正式但却严格的标准和紧张的迭代过程,《设计模式》中的23个模式正是基于这样的标准,经历了这样的迭代过程产生的。知道有这样的过程,以及这样的过程如何发生,让人感到释然,因为它把模式带回到更加讲究实用的日常工作中。我认为这将有助于读者认识到必须根据手头的问题来对模式进行调整,有助于读者加入自己的思考而不仅仅是盲目地遵循“某本书中说过的”教条。我不认为计算机科学家会喜欢这本书,但现实的程序员会反复品味,获得共鸣,并高度欣赏它。James O.Coplien朗讯科技公司贝尔实验室前言

我永远不会忘记 1994 年秋天的那个下午。那天我收到一封来自 Stan Lippman(时任C++ Report杂志的主编)的电子邮件,他邀我为该杂志撰写一个专栏,该专栏每两个月一期。

我们算得上是老相识了,早在他参观Watson实验室的时候我们就认识了。那一次我们简单地聊了他在开发工具方面所做的工作,以及 GoF 在模式方面所做的工作。与那时大多数人不一样的是,Stan熟悉模式的概念——他接连阅读了《设计模式》的一些预览本,并说过一些令人鼓舞的话。尽管如此,我们的谈话很快就转移到了写作上。随着谈话的进行,我记得自己愈加炫耀起来,仿佛我已经不是自己了。而Stan,这位知名的专栏作家,是两本非常成功的图书(还有一本即将出版)的作者,却称自己的写作只是业余水平。我不清楚我们的谈话是否让他感到愉快,还是在他的下一个约会之前他一直都在耐着性子和我谈话。(此后我认识到,如果还有什么能胜过Stan的忍耐力,那就是他的真诚!)

几个月后我收到他的电子邮件,心潮起伏,此前的歉疚感就不值一提了。想象着自己为全球的读者定期撰写专栏,这既让我兴奋,又让我恐惧。写了几次之后我是否还能继续?人们是否在乎我写些什么?我应该写些什么?我写的东西对别人是否有帮助?

我在恐惧中沉溺了将近一小时。然后我想起我父亲的一些告诫:局促不安只能使人无所作为。只要关注最基本的东西,其他东西会随之而来的。“只管去做”(Just do it),他说这句话比耐克要早得多。

于是我就接受了。

选择专栏主题非常容易。那时我深陷于模式的研究中已有三年了。我们最近刚完成《设计模式》,但我们都知道它远远没有说完这个话题。专栏会是一个很好的论坛,可以对《设计模式》一书进行解释,可以对它进行扩展,还可以在新问题出现时展开讨论。如果说专栏有助于《设计模式》图书的销售,那也无妨,只要它立场公正,不乱吹嘘。

现在,我的“模式孵化”专栏已经连载了10多篇文章了,回过头看,我的恐惧是没有依据的。我从来没有因为要找东西写而为难,而且写作时我乐在其中。我还从世界各地收到了大量令人鼓舞的反馈,包括一些人要求阅读过去的专栏,而且这样的要求一再出现。后来我想到了把我的专栏,以及其他一些尚未发表的关于模式的材料,汇编在一起提供给大家。

本书就是要达到这个目的。读者将在书中找到我前三年专栏写作生涯中的思考和想法,其中包括发表在C++ Report和Object Magazine中的所有文章,加上一些零碎的新见解。我按照逻辑的顺序来组织内容,而不是通过时间顺序来组织内容,其目的是为了使所有的内容能够像书本一样连贯。这样的组织比我想象的要容易一些,因为许多文章既是这个系列的一部分,又是那个系列的一部分,当然这仍然需要耗费大量的精力。我衷心地希望读者能够喜欢最终的结果。

致谢

一如既往,我要感谢许多人为我提供各种各样的帮助。首先最重要的是我的GoF 成员——Erich Gamma、Richard Helm以及Ralph Johnson。他们每一个人都在不同的时刻为我提供了宝贵的反馈,这些反馈汇集在一起使本书成为了一本更加不同(当然是更好)的图书。我们几人互补性强,如同天成,遇见他们是我三生有幸,我由衷地感谢他们。

然而,同样的帮助也来自其他人。还有许多人花时间研读草稿,为的是找出不合逻辑的论述、不当的言辞,以及大家都再熟悉不过的笔误。他们是Bruce Anderson、Bard Bloom、Frank Buschmann、Jim Coplien、Rey Crisostomo、Wim De Pauw、Kirk Knoernschild、John Lakos、Doug Lea、Bob Martin、Dirk Riehle以及Doug Schmidt。特别感谢Jim,他是我在C++ Report 的拍挡,不仅因为他为本书作序,更因为他是如此多才多艺,总是激励我奋进。

接下来要感谢的完全是一些陌生人,他们给我发电子邮件问我问题,提出意见,纠正我的错误,并给我以善意的责备。有许多这样的人,在这里我只列出了本书引用了他们的话的人,或者他们的意见与本书直接相关的人:Mark Betz、Laurion Burchall、Chris Clark、Richard Gyger、Michael Hittesdorf、Michael McCosker、Scott Meyers、Tim Peierls、Paul Pelletier、Ranjiv Sharma、David Van Camp、Gerolf Wendland 和Barbara Zino。虽然很多人我没有提到名字,请相信我同样感谢你们的反馈。

最后,我要感谢两个家庭,一个是我自己的家人,另一个是我亲如一家的同事,你们对我的支持我无以言表。我欠你们的太多了。J.V.vlis@watson.ibm.com1998年1月于纽约州霍索恩市第1章介绍

在阅读本书之前,如果读者还没有听说过一本名叫《设计模式》(Design Patterns: Elements of Reusable Object-Oriented Software [GoF95])的书,那么现在正好可以去找一本来读。如果读者听说过该书,甚或自己还有一本但却从来没有实际研读过,那么现在也正好应该好好研读一下。

如果你仍然在继续往下阅读,那么我会假设你不是上述两种人。这意味着你对模式有大致的了解,特别是对23个设计模式有一定的了解。你至少需要具备这样的条件才能够从本书受益,这是因为它对《设计模式》中的材料进行了扩充、更新和改进。如果不熟悉所谓的 GoF 模式——也就是刚才提到的那 23个设计模式,那么读者将会很难理解书中的见解。事实上,在阅读这本书的时候,最好备一本《设计模式》在身边以便随时查阅。我还要假设你熟悉C++,这么假设应该很合理,因为我们在《设计模式》一书中也做了同样的假设。

现在让我们来实际检验一下,看你是否能用不到25个字来描述COMPOSITE模式的意图。你可以考虑一分钟。

※ ※ ※

在描述COMPOSITE模式的意图时,你是不是句斟字酌?如果确实如此,好,没问题。其实你大可不必把这个小测验看得太严重了,不妨放松一点。如果你知道该模式的意图,但只是无法准确描述出来,那么不必担心——本书同样适合你。

但如果你的头脑中完全是一片空白,那么很不幸,我的开场白显然没有起作用。我建议你放下本书,拿起一本《设计模式》,从第163页开始阅读,直到读完“实现”那一节。对该书第xv页中列出的其他模式,也采取同样步骤。如此一来,你就可以对背景知识有足够的了解,从而使得本书对你有用。

你可能会想,为什么本书会取名为 Pattern Hatching?我最初选择这个名字,是因为计算机科学中有类似的概念。(此外,与模式有关的好书名都已经被用了。)但此后,我逐渐认识到它非常好地表达了我写作的意图。Hatching之意并非创造,而是在现有的基础上进行拓展。用于此处非常贴切:如果把《设计模式》看成一盒鸡蛋,那么许多新生命将会在这里破壳而出。

请相信,我并不仅仅是效仿《设计模式》。我的目的是在它的基础上进行拓展,运用其中的概念,并使这些概念对读者更加有用。本书介绍了一些技巧,来帮助我们决定在不同的情况下,应该使用哪些模式以及不应该使用哪些模式。本书不仅对我们已有的一些模式提出了新的见解,还可以让读者见证我们开发新模式的整个过程。本书还提供了大量的设计示例,其中一些是久经考验的,另一些是试验性质的,或者说是“半成品”,还有一些则完全是主观推测——很可能是纸上谈兵的设计,根本经不起实践的检验,但它们也可能会蕴含未来健壮设计的种子。

我衷心地希望本书能够加深你对设计的感悟,提高你对模式的认识,并开阔你软件开发的视野。这些都是我在使用模式时曾有过的经历,希望它们也能成为你自己的宝贵经历。1.1对模式的十大误解

这些日子,模式引起了大家强烈的兴趣,同时还伴随着一些迷惑、诧异和误解。这在一定程度上体现了主流软件开发人员认为这个领域有多么新,虽然从严格意义上说,它并不是一个新领域。这个领域的快速发展,也造成了一些空白。作为模式的倡导者,我们对此负有一定的责任:我们虽然一直努力让大家理解和接受模式([BMR+96、Coplien96、CS95、GoF95、MRB98和VCK96]),但是工作并不彻底。

为此,我感觉自己有义务来纠正那些对模式比较明显的误解,这些误解我常常耳闻,甚至可以自成模式了。我甚至还开玩笑地采用模式的形式来表述它们……直到那一刻我幡然醒悟:将任何事物都归纳为模式,这种行为本身就是对模式的一种误解!无论如何,请记住我并不是代表模式社区在发言。虽然我认为大多数模式专家都会同意这些是对模式最常见的误解,但就如何消除这些误解而言,他们的意见可能会与我的相左。

这些年人们对模式众说纷纭,令我反复思考,众多误解不过分为三类:一类有关模式是什么,一类有关模式能够做什么,还有一类有关一直以来在推动模式的社区。我所列举的“十大”误解都可以被归到这三类中。因此,我会将它们分门别类。首先来看看关于模式是什么的误解。

误解1:“模式就是在一种场合下对某个问题的一个解决方案。”

这是Christopher Alexander的定义[AIS+77],因此把它算作一种误解可能会显得有些离经叛道。但下面这个反例应该能够显露出它的不足。

问题:如何在过期之前兑现中的彩票?

场合:离最后期限只有一小时,一条狗把彩票吃了。

解决方案:剖开狗的肚子,取出彩票,然后飞奔到最近的兑现点。

虽然这是在一种场合下对一个问题的解决方案,但它并不是一个模式。那它缺少了什么呢?至少需要三样东西。(1)再现(recurrence),这使得该解决方案不仅与当前场合下的问题有关,而且与当前场合之外的问题也有关。(2)教学(teaching),这将教会我们去理解怎样对解决方案加以完善,从而适应问题的变体。(对实际使用的模式来说,与教学有关的大部分内容都包含在对问题的描述、对解决方案的描述以及应用模式后得到的结果中。)(3)一个用来指代模式的名字。

诚然,一个令所有人都满意的定义是很难找到的,从“pattern-discussion”邮件列表(即patterns-discussion@cs.uiuc.edu)中持续的争论可略知一二。其中的困难在于,模式既是事物又是对相似事物的描述。区分两者的一种方法是,统一使用术语模式来表示描述,用模式实例来表示对模式的具体运用。

但是,定义术语可能只是徒劳无功,因为一个定义也许对一部分受众(如程序员)有用,但对另一部分受众(比如掌管公司财政大权的执行官)来说却毫无意义。当然,我也不会尝试在这里给出一个最终的定义。我只想说,任何一个规定模式的组成要素的定义,除了要讨论问题、解决方案和场合之外,还必须涉及再现、教学以及命名。

误解2:“模式只是行话、规则、编程技巧、数据结构……”

我称这种误解为“不以为然”。诚然,将不熟悉的事物归纳成已知的事物对我们来说是一件很自然的事情,尤其是在我们没有兴趣对不熟悉的事物进行深入研究的时候。再者,用新瓶装旧酒并号称创新的事情我们已经屡见不鲜了。保持警惕是应该的。

然而,“不以为然”并没有经验依据,很多时候它只是基于表面相似性的一种看法,还掺和些许轻视的态度。此外,从来没有什么东西是全新的,其实自从每个人出生起,各种模式就已经存在于他们的脑子里了。新的只是我们开始对模式进行命名并把它们记载下来。

来看看上面这句话。事实上的确存在一些模式行话,比如“模式”(pattern)本身、“推动力”(force)、Alexander的“无名的品质”(quality without a name),等等。但我们很难把模式简单归纳成行话。与计算机科学中的大多数领域相比,模式几乎没有引入什么新术语。事实上这是模式的特征,一个好的模式天生就很容易为它的受众所理解。虽然模式可能会用到它所面向的目标领域的行话,但我们几乎不需要为模式定义专门的术语。

模式不是可以盲目应用的规则(否则有悖于模式的教学特性)。模式也不仅仅是编程技巧,虽说“惯用法”关注的是与特定的编程语言有关的模式。“技巧”在我听起来有些贬义,它过分强调了解决方案,而忽略了问题、场合、教学以及命名。

毫无疑问,一项新事物要被接受会经历三个阶段:第一个阶段,它被当作垃圾,无人问津;然后它好像是不可行,无法推广;最后它变得显而易见,理所当然,人们会说:“我们一直以来都是这么做的。”模式目前还没有完全脱离第一阶段。

误解3:“看到了冰山的一角,就等于看到了冰山的全部。”

以偏概全不是一种正当的做法,如果用这种方式来看待模式,那就大错特错了。模式所涉及的领域、内容、范畴和风格非常广泛,而且它们的质量也千差万别。只要随便翻阅Pattern Languages of Program Design [CS95、MRB98、VCK96] 丛书中的一本,就可以感受到这一点。模式和编写模式的人一样多样,也许有过之而无不及。随便举几个例子,Alistair Cockburn、Jim Coplien、Neil Harrison以及Ralph Johnson 等,虽然这些作者一开始也曾尝试用多种风格来为不同领域编写模式,但是现在他们早已经超越了这个阶段。因此,仅仅通过少数几个例子就对模式下一个笼统的结论是错误的。

误解4:“模式需要工具或方法的支持才能生效。”

在过去的5年中,我曾经编写过模式,使用过模式,并帮助过别人使用模式,也参与设计过至少一个基于模式的工具[BFY+96]。我可以很有把握地说,模式的优点来自于对模式本身的应用,也就是说不需要任何形式的支持。

当我在谈论这个话题的时候,我通常会指出模式的4个主要优点。(1)它们提炼出专家的经验和智慧,为普通开发人员所用。(2)它们的名字组成了一个词汇表,有助于开发人员更好地交流。(3)系统的文档若记载了该系统所使用的模式,则有助于人们更快地理解系统。(4)它们使得对系统进行改造变得更加容易,无论系统原来的设计是否采用了模式。

长久以来我原本认为大部分的优点来自第1点。现在我认识到第2点的重要性不亚于第1点。想一想:在软件开发的过程中,开发人员之间的口头及电子形式的交流的信息量有多少字节?我猜即使没有几吉字节,也有好几兆字节。(在我们编写《设计模式》一书的过程中,我保存下来的我们4人之间来往的电子邮件文件大小达数兆字节之多。我认为我们所付出的精力,差不多相当于开发一个小型到中等规模的软件项目。)交流如此多耗时自然多,任何有助于提高交流效率的东西都将为我们节省相当可观的时间。因此,模式使人与人之间的交流更顺畅高效。随着软件开发项目的规模变得越来越大,软件的寿命变得越来越长,我对第3点和第4点的重视程度也在不断提高。

简而言之,模式是供大脑消化吸收的食粮,而不是供工具加工的材料。方法论和自动化的支持对模式可能会有好处,但我相信这些都只是锦上添花的东西而已。

※ ※ ※

我们到目前为止讨论的误解都与模式是什么有关。现在让我们来讨论一些关于模式能做什么的误解。这些误解有两类:夸大其词类和轻描淡写类。

误解5:“模式可以保证可复用的软件、更高的生产率、世界和平,等等。”

这个误解很容易批驳,因为模式并没有保证任何东西。它们甚至不能增加从中获取益处的可能性。模式并不能代替人来完成创造,它们只不过给那些缺乏经验但却具备才能和创造力的人带来了希望。

人们说到好的模式,会有恍然大悟之感。只有当模式能够触动心弦时,这种情况才会发生。如果模式无法触动心弦,那么它就像人迹罕至的森林中的一棵大树,纵使轰然倒下也没有人能听到它的声音。模式也是如此:即便它编写得再好,如果不能引起人们的共鸣,那么它好在哪里呢?

模式只不过是开发人员军火库中的另一件武器。将太多东西都归功于模式只能适得其反。要防止夸大其词的宣扬引发抵触情绪,最好的方法就是——少说多做。

误解6:“模式可以‘产生’整体架构。”

这种误解与上一种误解很相似,只不过夸张的程度要轻一些。

在模式的论坛里,定期会有一些关于模式的产生能力(generativity)的讨论。我认为,产生能力指的是模式能够创造新行为(ermergent behavior)的能力。这种表述听起来很酷,其意思是模式能够帮助读者解决模式没有明确解决的一些问题。就我所知,还有一些观点认为,真正的产生能力几乎能够自动实现这一点。

对我来说,产生能力的关键在于模式与教学相关的部分,例如,对问题的描述和对解决方案的描述,或对效果的讨论。在定义和提炼架构的时候,这些见解尤其有用。但模式本身并不能产生任何东西,能够产生东西的是人,只有当人具备足够的经验并且他们使用的模式足够好的时候,他们才能够这样做。而且,模式不可能涵盖架构的方方面面。给我看一个稍有规模的设计,我一定能发现既有模式尚未涉及的许多设计问题。也许这些问题不常见或不经常发生,或者只不过它们尚未被编写成模式的形式。但无论如何,我们需要运用自己的创造力来填补各种现有模式之间的空白地带。

误解7:“模式是用于(面向对象的)设计或实现的。”

误解的另一个极端是过分贬低模式的作用,就像现在讨论的这一种。竟然有人会相信这种说法,坦白地说,我对此感到很惊讶。然而有许多人曾经就这个问题问过我,多到足以让它能够在十大误解中占有一席之地。如果你觉得这种误解太过幼稚,那么可以直接跳到下一种误解。

如果模式不能把专家的经验记录下来,那么它们就一无是处。究竟记录哪些经验则由模式的编写者决定。在面向对象的软件设计中,当然有值得记录的经验,但在非面向对象的设计中,同样有值得记录的经验。不仅在设计领域有值得记录的经验,而且在分析、维护、测试、文档、组织结构等领域都有值得记录的经验。这些不同领域中的模式正在浮现。目前在分析模式领域,已经至少出版了两本书[Fowler97, Hay96],而且每一届的PLoP会议都会吸引一些新型的模式。(提交给1996年会议的一个模式特别有意思,它是关于音乐作曲的模式!)

与大多数的误解一样,这种误解也有一定的道理。如果看一看人们使用模式的形式,就会发现两种基本的风格:一种是《设计模式》一书所使用的高度结构化的GoF 风格,另一种是Christopher Alexander的近乎纯文学的风格——叙述的文体,采用尽可能少的结构。在我涉足为面向对象设计以外的领域编写模式之后,才认识到GoF风格是多么地偏向面向对象领域。在我尝试过的其他领域,GoF风格根本不适用。对C++惯用法来说,它的结构图应该是什么样的?对音乐作曲的模式来说,它在实现上的取舍应该是什么样?对于用来撰写好的说明文的模式来说,它的协作部分应该是什么样?

显然,没有任何一种模式能够适用于所有领域。唯一能够适用于任何领域的是一个通用的概念——无论在什么领域,模式都是一种用来记录和传播专家经验的工具。

误解8:“没有证据表明模式对任何人有帮助。”

这种误解在过去还能站得住脚,但现在已经不是那么回事了。人们正在通过各种渠道报道模式所带来的好处,这些渠道包括 Software—Practice and Experience [Kotula96]之类的期刊,以及 OOPSLA [HJE95, Schmid95]和 ICSE [BCC+]之类的会议。Doug Schmidt也曾经表示过,模式对大学生和研究生的计算机科学教学有诸多好处[PD96]。虽然这些大多是定性的分析,但就我所知,至少有一个团体正在进行受控的实验,以获取量化的结果。

随着时间的推移,我们会更加清楚使用模式所带来的好处和隐患。即使最初的反馈非常好,我们仍然需要积累更多的经验,这样才能得到一个更加全面的评估。但是,如果仅仅因为模式所带来的好处还没有被完全量化就拒绝马上开始使用模式,那绝对是很愚蠢的行为。

※ ※ ※

关于模式能够做什么的谬论就到此为止。下面最后两种误解与模式本身无关,而与拥护模式的社区有关。

误解9:“模式社区是一个由精英分子组成的小帮派。”

我很想知道这样的想法从何而来,这是因为如果模式社区确实有哪方面值得一提,那一定是它的多样性。这一点很容易判断,只要看一看 PLoP 的与会者就可以知道——人们来自世界各地,既有来自大公司也有来自小型创业公司,有分析师、设计师和实现者,有学生和教授,还有大名鼎鼎的作者和新手。而且令我感到惊讶的是,有几个经常参加该会议的与会者竟然不是搞计算机的!模式社区仍然处于不断变动的状态,每年与会者的流动率都相当高。

模式社区里常常有著作发表,但社区中有学术背景的人相对来说却并不多,对此有人可能会感到不解。事实上,PLoP的大多数与会者都是软件行业的从业人员,而且似乎一直以来都是这样。软件模式的早期拥护者们(包括Kent Beck、Peter Coad以及Ward Cunningham)没有一个是来自学术界的。GoF中只有一个(Ralph)来自学术界,而且他是我所见过的最讲究实用性的学者。模式社区的草根本质显然与那些所谓的同种论(homogeneity)和精英论是背道而驰的。

误解10:“模式社区是为自己服务的,甚至是不怀好意的。”

我曾经不止一次听到对模式的责难,说模式的主要用途是作为那些编著模式方面图书的人的收入来源。甚至还有一种说法是模式正朝着一个不可告人的方向发展。

完全是一派胡言!

作为GoF中的一员,我可以非常肯定地说,我们4人和其他任何人一样,对于人们对《设计模式》的反响感到惊讶。毫无疑问,当设计模式在1994年的OOPSLA会议上初次亮相时,我们4人对它所引起的轰动效应完全没有心理准备,读者的大量需求甚至让出版社都感到措手不及。在写书的整个过程中,我们最多的考虑就是尽我们所能来创造一本最高质量的图书。为了深入理解模式的内容,我们已经太忙了,根本无暇考虑销售问题。

当时的情况就是那样。现在模式已经成为了一个重要的术语,因此有些人想利用它来谋取一些私利也在所难免。但是,如果仔细地阅读那些模式领军人物编写的作品,你就会感受到其中共同的宗旨:将来之不易的专家经验、最佳实践,甚至是竞争优势——多年亲身实践所积累的经验硕果——不仅展露出来而且传授给所有后来者。

正是这种要提升所有读者的软件设计能力的热情,激励着每一位真诚而富有成效的模式编写者。缺少任何一项因素,那只能是适得其反,并最终导致对模式的误解。1.2观察

澄清了这些误解之后,人们对设计模式的反应不外乎有两种。下面我会通过一个类比来对它们进行描述。

设想有一个电子爱好者,虽然他没有经过正规的培训,但是却日积月累地设计并制造出了许多有用的电子设备:业余无线电、盖革计数器、报警器等。有一天这个爱好者决定重新回到学校去攻读电子学学位,来让自己的才能得到正式的认可。随着课程的展开,这个爱好者突然发现课程内容都似曾相识。似曾相识的不是术语或表述的方式,而是背后的概念。这个爱好者不断学到一些名称和原理,虽然这些名称和原理原来他并不知道,但事实上他多年以来一直都在使用。整个过程只不过是一个接一个的顿悟。

现在让我们把镜头切换到一个大学新生,这个新生正在同一个班学习同样的课程。他没有电子学的背景,有的只是大量的轮滑经验,没错,但就是没有电子学的背景。对他而言,学习新课程极其吃力,这并不是因为他笨,而是因为这些内容对他来说完全是新的。这个新生需要花更多的时间来理解和领会所有的内容。通过努力学习再加上一点毅力,他最终完成了所有的课程。

如果你觉得自己像一个设计模式爱好者,那么你会更加有动力。如果你觉得自己更像一个新生,那么请振作起来:你在学习好的模式上的付出是不会白费的,只要将它们应用到自己的设计中就会得到回报。我保证。

但对有些人来说,电子学这个类比可能不太贴切,因为其中包含了“电子技师”的内涵。如果你也这样认为的话,那么不妨考虑一下 Alfred North Whitehead在1943年说过的一句话,虽然是在不同的场合下说的,但它也许会更加贴切:

艺术就是将一种模式强加于经历,以及识别这种模式时所带来的审美享受。第2章运用模式进行设计

如果想体验一下运用模式的感觉,那么最好的方法就是运用它们。对我来说,最大的挑战在于找到一个所有人都能够理解的示例。人们对自己的问题最感兴趣,如果某些人对某个示例越感兴趣,这个示例往往就越具体。问题在于,这样的示例所涉及的问题往往太过晦涩,对于没有相关领域背景的人来说难以理解。

层级文件系统(hierarchical file system)是每个计算机用户都熟悉的东西,就让我们来看看该如何设计它。我们不会关心诸如I/O缓冲和磁盘扇区管理之类的底层实现问题,我们要关心的是设计一个让应用程序开发人员使用的编程模型——文件系统的API。在大多数的操作系统中,这样的API通常包含大量的过程调用和一些数据结构,但对扩展性的支持却很少或根本没有。我们的设计将完全是面向对象的而且是可扩展的。

我们首先集中讨论这个设计最重要的两方面,以及用来对这两方面进行处理的模式。然后我会在这个示例的基础上展示其他模式是如何解决设计问题的。本章的目的并不是要为如何应用模式规定一个严格的流程,也不是要展示设计文件系统的最佳方法,而是要鼓励读者自己应用模式。用得越多,看得越多,你在处理模式的时候就会感到越轻松。最终,你将慢慢地学会应用模式所需的精湛技艺:属于你自己的技艺。2.1基础

从用户的角度来看,无论文件有多大,目录结构有多复杂,文件系统都应该能够对它们进行处理。文件系统不应该对目录结构的广度或深度施加任何限制。从程序员的角度来看,文件结构的表示方法不仅应该容易处理,而且应该容易扩展。

假设我们正在实现一个用来列出一个目录中文件的命令。我们编写的用来得到一个目录的名字的代码与用来得到一个文件的名字的代码相比,应该没有区别,也就是说,同样的代码应该能够同时处理这两种情况。换句话说,在请求目录的名字和文件的名字时,应该能够以相同的方式处理。这样得到的代码将会更易于编写和维护。我们还想在不重新实现部分系统的前提下,加入新的文件类型(比如符号化链接)。

因此,一开始有两件事情非常清楚:一是文件和目录是这个问题域(problem domain)的关键元素,二是我们需要一种方式,能够让我们在完成设计之后再为这些元素引入特别的版本。一种显而易见的设计方法是用对象来表示这些元素。

我们如何实现图2-1所示的结构呢?有两种对象,这意味着我们需要两个类——一个用来表示文件,另一个用来表示目录。我们还想以同样的方式处理文件和目录,这意味着它们必须有一个共同的接口。更进一步说,这意味着这两个类必须派生自一个共同的(抽象)基类,我们称之为Node。最后,我们还知道目录中包含文件。图2-1

所有这些约束基本上已经替我们把类的层次结构定义出来了。

Class Node {

public:

// declare common interface here

protected:

Node();

Node(const Node&);

};

Class File : public Node {

public:

File();

// redeclare common interface here

};

Class Directory : public Node {

public:

Directory();

// redeclare common interface here

private:

list _nodes;

};

另一个需要斟酌的问题与共同接口的组成有关。哪些操作既能够适用于文件又能够适用于目录呢?

文件和目录有各种各样的共同属性,比如名字、大小、保护属性等。每个属性可以有相应的操作来访问和修改它的值。以相同的方式来处理那些对文件和目录都有明确意义的操作是很简单的事情。但是,想以相同的方式来处理那些不能明确适用于两者的操作时,问题就随之而来。

举个例子,用户经常执行的一项操作就是列出一个目录中的所有文件。这意味着Directory需要一个接口来枚举它的子节点。下面这个简单的接口用来返回第n个子节点。

virtual Node* getChild(int n);

由于一个目录既可能包含File对象,也可能包含Directory对象,因此getChild必须返回一个Node*。这个返回值的类型衍生出一个重要的结果:它强制我们不仅要在Directory类中定义getChild,而且还要在Node类中定义该接口。为什么?因为我们想要能够列出子目录的子节点。实际上,用户经常想要访问文件系统结构的下一层。除非不用强制转换就能用getChild的返回值来调用getChild,否则是无法通过一种静态的、类型安全的方式来完成这个操作的。因此,和属性操作一样,getChild是我们想要同时用在文件和目录上的操作。

同时,getChild也是允许我们以递归的方式来定义Directory的操作的关键。假设Node声明了一个size操作,这个操作返回该目录树(及其子树)所占用的总字节数。Directory可以这样定义自己的这个操作:依次调用它所有子节点的size操作,将所有的返回值相加,得到的总和就是自己的返回值。

long Directory::size() {

long total = 0;

Node* child;

for (int i = 0; child = getChild(i); ++i) {

total += child->size();

}

return total;

}

目录和文件的例子说明了COMPOSITE模式最关键的几个方面:它产生的树结构可以支持任意的复杂度,它还规定了如何以统一的方式来处理这些树结构中的对象。COMPOSITE模式的意图部分对这些方面进行了描述:

将对象组织成一个树结构来表示“部分—整体”的层次结构,给客户一种统一的方式来处理这些对象,无论这些对象是内部节点(internal node)还是叶节点(leaf)。

适用性部分描述了我们应该在以下场合使用COMPOSITE模式。

我们想要表示对象的“部分—整体”层次结构。

我们想让用户能够忽略复合对象和单个对象之间的区别。用户将以统一的方式来处理复合结构中的所有对象。

该模式的结构部分用一个经过修改的 OMT图的形式,描绘了典型的COMPOSITE 类结构。之所以说它是典型,我的意思只是它代表了我们(GoF)所见过的类的最为常见的组织方式。它并不能代表最终得到的各个类及其关系,这是因为有时候受到某种设计或实现的影响,我们必须采取一些折中,这种情况下得到的接口可能会有所不同。(COMPOSITE模式同样对这些内容进行了阐述。)

图2-2展示了COMPOSITE模式涉及的各个类,以及这些类之间的静态关系。我们的Node类相当于Component,它是一个抽象基类。File类相当于子类Leaf,而Directory类则相当于子类Composite。从Composite指向Component的箭头线表明Composite包含了Component类型的实例。箭头前面的实心圆圈表示多于一个实例;如果没有实心圆圈,则表示有且仅有一个实例。箭头线尾部的菱形表示Composite聚合了它的子实例,这也意味着删除一个Composite会同样删除它的子实例。它还意味着所有的Component没有被共享,因此确保了树结构。COMPOSITE模式的参与者和协作部分对各个类之间的静态关系和动态关系分别进行了解释。图2-2

COMPOSITE 的效果部分总结了使用该模式的好处和坏处。好处是, COMPOSITE支持任意复杂度的树结构。这个特性产生的直接结果就是对客户代码隐藏了节点的复杂度:他们无法辨别出他们正在处理的Component到底是一个Leaf还是一个Composite,事实上他们也没有必要去辨别,这使得客户代码更加独立于Component的代码。客户代码也变得更加简单,因为它能够以统一的方式来处理Leaf和Composite。客户代码再也不需要根据Component的实际类型来决定要执行许多代码分支中的哪一个分支。最好的是,我们可以添加新的Component类型而无须修改已有的代码。

但是COMPOSITE的坏处在于它可能会产生这样的系统:系统中每个对象的类与其他对象的类看起来都差不多。由于显著的区别只有在运行的时候才会显现出来,因此这会使代码难以理解,即便我们知道类的具体实现也无济于事。此外,如果在一个比较低的层次运用该模式,或者运用该模式时的粒度太细,那么对象的数量可能会多得让系统负担不起。

正如读者可能已经猜到的那样,COMPOSITE模式的实现部分讨论了在实现该模式时会面临的许多问题。

为了提高性能,应该在何时以及何处对信息进行高速缓存;

Component类应该分配多少存储空间;

在存储子节点的时候,应该使用什么数据结构;

是否应该在Component类中声明那些用来添加和删除子节点的操作;

等等。

在开发我们的文件系统时,我们将努力解决这些问题中的一部分,以及许多其他问题。2.2孤儿、孤儿的收养以及代用品

现在让我们深入研究一下在我们的文件系统中运用COMPOSITE模式可能会得到什么样的结果。我们首先考察在设计 Node 类的接口时必须采取的一个重要折中,接着会尝试给刚诞生的设计增加一些新功能。

我们使用了COMPOSITE模式来构成文件系统的主干。这个模式向我们展示了如何用面向对象的方法来表示层级文件系统的基本特征。这种模式通过继承和组合来将它的关键参与者(Component、Composite及Leaf类)联系在一起,从而支持任意大小和复杂度的文件系统结构。它同时使客户能够以统一的方式来处理文件和目录(以及文件系统中可能出现的任何其他东西)。

正如我们已经看到的那样,统一性的关键在于为文件系统中的对象提供一个共同的接口。到目前为止我们的设计中已经有了三种对象类:Node、File和Directory。我们已经解释了需要在Node基类中定义那些对文件和目录都有明确意义的操作。用来获取和设置节点的名字和保护属性的操作就属于这一类。我们还解释了,虽然有一个用来访问子节点的操作(getChild)乍一看对 File对象并不合适,但为什么我们仍然需要把它放在共同的接口中。现在让我们来考虑其他一些看上去更没有什么共通之处的操作。

※ ※ ※

小孩子们是从哪里来的?虽然这听起来像是一个早熟的 5 岁小孩问的问题,但是我们仍然需要知道。(我猜在任何场合下这都是个不错的问题。)在一个Directory对象能够枚举它的子节点之前,它必须通过某种方式获得子节点。但是从哪里获得呢?

显然不是从它自己身上。把一个目录可能包含的每个子节点创建出来不应该是目录的责任,这样的事情应该由文件系统的用户来控制。让文件系统的用户来创建文件和目录并把它们放到相应的地方,这才是比较合理的做法。这意味着Directory对象将会收养(adopt)子节点,而不是创建子节点。因此,Directory需要一个接口来收养子节点。类似下面的接口就可以:

virtual void adopt(Node* child);

当客户代码调用一个目录对象的adopt函数时,就等于是明确地把管理这个子节点的责任转交给该目录对象。责任意味着所有权:当一个目录对象被删除的时候,这个子节点也应该被删除。这就是Directory和Node类之间(在图2-2中用菱形表示)的聚合关系的本质。

现在,如果客户代码可以让一个目录对象承担管理一个子节点的责任,那么应该还有一个函数来解除(relinquish)这份责任。因此我们还需要另外一个接口:

virtual void orphan(Node* child);

在这里“orphan”并不意味着它的父目录已经死了——被删除了,它只不过意味着该目录对象不再是这个子节点的父目录。这个子节点仍将继续存在,也许它马上就会被另一个节点收养,也许它会被删除。

这和统一性有什么关系?为什么我们不能把这些操作只定义在Directory中?

好吧,假设我们就是这样定义的。现在考虑一下客户代码如何实现改变文件系统结构的操作。一个用来创建新目录的用户级命令就是此类客户代码的一个例子。这个命令的用户界面无关紧要,我们可以假设它只不过是一个命令行界面,类似Unix的mkdir命令。mkdir有一个参数,用来传入待创建目录的名字,如下面所示:

mkdir newsubdir

事实上,用户可以在名字前面加上任何有效的路径。

mkdir subdirA/subdirB/newsubdir

只要subdirA和subdirB已经存在而且是目录而不是文件,那么这条命令就应该能够正确执行。更概括地说,subdirA和subdirB应该是Node子类的实例,而且可以有子节点。如果这一点不成立,那么用户应该得到一条错误消息。

我们怎么实现 mkdir呢?首先,我们假设 mkdir能够找出当前的目录是什么,也就是说它能得到一个与用户的当前目录相对应的Directory对象。给当前目录增加一个新目录只不过是小事一桩:先创建一个 Directory 实例,然后调用当前目录对象的adopt函数,并将新目录作为参数传入。

Directory* current;

// ...

current->adopt(new Directory("newsubdir"));

就是这么简单。但一般情况下传给mkdir的不仅仅只是一个名字,而是一个路径,我们应该怎样处理这种情况呢?

事情从这里开始变得有些棘手了。mkdir必须(1)找到subdirA对象(若该对象不存在则报告一个错误);(2)找到subdirB对象(若该对象不存在则报告一个错误);(3)让subdirB收养newsubdir对象。

第1点和第2点涉及对当前目录的子节点进行遍历,以及对subdirA(如果它存在的话)的子节点进行遍历,其目的是为了找到代表subdirB的节点。

在mkdir实现的内部,可能会有一个递归函数,该函数以路径作为它的参数。

void Client::mkdir (Directory* current, const string& path) {

string subpath = subpath(path);

if (subpath.empty()) {

current->adopt(new Directory(path));

} else {

string name = head(path);

Node* child = find(name, current);

if (child) {

mkdir(child, subpath);

} else {

cerr << name << " nonexistent." << endl;

}

}

}

这里head和subpath是字符串处理例程。head返回路径中的第一个名字,而subpath则返回剩余的部分。find操作在一个目录中根据指定的名字查找对应的子节点。

Node* Client::find (const string& name, Directory* current) {

Node* child = 0;

for (int i=0; child = current->getChild(); ++i) {

if (name == child->getName()) {

return child;

}

}

return 0;

}

值得注意的是,由于getChild返回的是Node*,因此find也必须返回Node*。这并没有什么不合理的地方,因为子节点既可以是一个Directory也可以是一个 File。但是如果仔细地阅读代码,就会发现这个小小的细节对Client::mkdir有着致命的影响——Client::mkdir是无法通过编译的。

让我们再看一下对mkdir的递归调用。传给它的是Node*,而不是所需的Directory*。问题在于,当我们深入访问文件系统的层级时,我们并不知道一个子节点到底是文件还是目录。一般来说,只要客户代码不关心这种区别,这就是一件好事。但在目前的情况下,看起来我们确实需要关心这种区别,因为只有Directory才定义了用来收养子节点和遗弃子节点的接口。

但我们真地需要关心这一点吗?或者更进一步说,客户代码(mkdir命令)需要关心这一点吗?不一定。它的任务是要么创建一个新目录,要么向用户报告错误。因此让我们假设,只是假设一下,我们对所有的 Node 类都以统一的方式来处理adopt和orphan。

好了,好了。我知道你在想,“天啊!这些操作对 File 之类的叶节点来说毫无意义。”但这样的假设是不是切合实际呢?如果今后有人想定义一种新的类似垃圾箱(说得更准确一些,是回收站)的叶节点,它可以销毁它收养的所有子节点,那么这种情况该怎么处理?如果想在叶节点收养子节点时产生一条错误消息,那么这种情况又该怎么处理?我们很难证明adopt对叶节点来说绝无意义,orphan同样也是如此。

另一方面,有人可能会争辩说一开始就没有必要把File类和Directory类分开——所有的东西都应该是Directory。这样的论点是合理的,但是从实现的角度来说,它存在一些问题。一般来说,Directory对象中的许多内容对大多数文件来说是不必要的,比如用来存储子节点的数据结构、用来对子节点信息进行高速缓存以提高性能的数据结构,等等。经验表明,在许多应用程序中,叶节点的数量通常要比内部节点的数量多得多。这也是为什么COMPOSITE模式要把Leaf和Composite类分开的原因。

让我们来看一看,如果我们不仅仅只在 Directory 类中定义 adopt 和orphan,而是在所有的Node类中定义adopt和orphan,那将发生什么情况。我们让这些操作在默认的情况下产生错误消息。

virtual void Node::adopt (Node*) {

cerr << getName() << " is not a directory." << endl;

}

virtual void Node::orphan (Node* child) {

cerr << child->getName() << " not found." << endl;

}

虽然这些并不一定是最好的错误消息,但是应该足以让读者领会其中的含义。除了产生错误消息之外,这些操作还可以抛出异常,或者什么也不做——我们有许多选择。现在无论在什么情况下,Client::mkdir 都可以完美地执行。同时请注意,这种方法不需要对File类做任何改动。当然,我们必须修改Client::mkdir,在参数中用Node*来代替Directory*。

void Client::mkdir (Node* current, cosnt string& path) {

// ...

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载