我认为这本书是计算机科学中最经典、最重要的著作之一--“设计模式:面向重用对象设计的元素”(Design Patterns:Elements of Reuse Object Oriented Design)。这是不幸的,因为前一年是这本书出版25周年,一本如此古老的书能产生如此尖酸刻薄的言辞是很奇怪的。尤其是推特上的人对这本书相当粗暴-使用了像“无法穿透”这样的词,并在一个令人难忘的降级中使用了“计算机科学的地图集耸耸肩”(The Atlas Shimed Of Computer Science)。
从某种意义上说,我对此并不感到惊讶。即使当这本书出版时,它似乎也有和支持者一样多的批评者。更重要的是,即使是在写这本书的时候,当时也有一小群热情的评论家认为,在出版之前,这本书需要完全重写。但每本书都有问题-设计模式也不能幸免于这样一个事实,即任何作品充其量只能不完美地反映作者的想法。
然而,在我看来,最难理解的是,不管对作品的格式、风格甚至字体选择有何评论,我一直不太明白的是,为什么有人会以琐碎、无用或平庸为由拒绝其中包含的想法。但话又说回来,这就是我作为一名程序员和一名作家的经历与我在过去25年中发现的最常见的情况不同的地方。
那么让我们从头开始吧。我首先学会了如何编程,就像我们这一代的许多人一样--我们在家用电脑上学习基本编程。对我来说,这是我在学校、在朋友家遇到的一连串的东西,最终我拥有了它们。在高中期间,我学会了用Integer和Applesoft Basic在Apple II上编程,在VIC-20上编程,在TI-99/4A上编程,在Sclair TX1000上编程。在Sclair上编程,只有4K,奇怪的Shift-键组合来创建基本的关键字,这与在Apple II相对宽敞的48K上编程的体验截然不同,苹果II的键盘触感极佳,需要你完整地键入所有命令。因此,我在Sclair上进行的低级开发比在Apple上做的要多得多,在Apple上,Applesoft Basic出色的浮点功能让我可以自由发挥自己的想象力。几乎从一开始,我就了解到你可用的环境和语言形成了你思考问题的方式。
除此之外,我学习编程的方式在当时是相当典型的-我是通过编写程序来学习的。现在,会有一些你可以阅读的程序-特别是那些在杂志上发表的程序,你必须键入它们才能运行它们(没有互联网!)。在少数几个可以免费获得并在BBS、磁盘和磁带上流传的(早在“开源”成为一件事之前很久),但总的来说,你学会了如何通过解决问题来解决问题。
当我去大学攻读学位时,我发现这仍然是学习编程和计算机科学中比较棘手的部分的主要范例。例如,当我上一堂关于操作系统的课程时,我们会读到一个概念,比如信号量或文件系统,然后被期望能够实现这个概念。文本中可能有几个简单的示例来帮助您了解实现如何工作,但通常这些示例是伪代码类型,您必须在头脑中将其转换为C或C++。
当我从大学毕业,开始从事C++和C程序员的工作时,与我在大学里学习东西的方式相比,没有什么太大的改变。每个编程问题都是从搜索相关库的文档开始的,然后坐下来规划如何从头开始编写解决方案。这正是我认为发展应该是这样的,而且永远都会是这样。
但是,在1989年冬天,我有了一个不同寻常的机会,改变了我思考如何解决问题的方式,从而改变了我对编程的思考方式。那个机会是在我被介绍给Smalltalk的时候(特别是Digitalk的Smalltalk/V PM版本)。用Smalltalk编写程序与传统的开发方式背道而驰,我花了很长时间才完全理解它。我发现,试图学习它的人最终会陷入两个极端之一-要么它成为他们一直以来最喜欢的开发语言和环境,要么他们以近乎狂热的热情讨厌它。
为了理解为什么这是真的,您需要了解一些事情,这些事情在当时对于Smalltalk来说是完全激进的,即使到了今天,也没有在任何其他开发语言或环境中完全复制。
1.Smalltalk没有您放置源代码的文件。现在,从某种意义上说,这是一个谎言,因为只有一个文件包含你的源代码,但它并不是按照你想象的方式组织起来的。与每个模块或类的单个文件不同,所有早期的Smalltalk实现都有一个称为更改日志的大型滚动日志文件,其中包含您添加到系统中的新源代码。当您添加新方法或修改方法时,它会添加到更改文件的末尾。2.Smalltalk没有基于文件的编译系统-相反,您对更改文件中的任何方法或类定义所做的每一次更改都会由增量编译器立即编译,并添加到整个Smalltalk系统的单个大型内存映像中。3.SmallTalk过去(现在也是)完全是面向对象的。它是面向对象的,Java、Python或C++(或更新的语言)甚至无法与之匹敌-因为即使是整数也是对象-而且,您可以通过向其添加新方法来扩展Integer的行为!4.Smalltalk有一个很棒的概念,称为工作区,它允许您突出显示和运行任何长度的小代码片段-它只需立即编译和运行它们。人们还会在一个工作区内混合搭配文档和代码片段--这是当今Jupyter笔记本背后的基本理念。这对于仅仅尝试一些东西看看它做了什么来说是很棒的--这是在探索中学习的过程的一部分。
但最后这一点只是冰山一角,我认为这是Smalltalk最具革命性的事情之一,它改变了我的发展方式,并影响了当时每个接触Smalltalk的人对发展的看法。冰山的其余部分是这样的-整个系统,从实现您在中开发的编辑器的代码,到像Integer和Collection类这样的基本系统库,再到Smalltalk编译器类本身,都可以在Image中(从单个基础的、不变的“源”文件)供您阅读!
正是这个功能改变了我对开发的想法--而这种改变只是通过教我如何使用Smalltalk来实现的。我得到了两位真正的语言大师-萨姆·亚当斯(Sam Adams)和肯·奥尔(Ken Auer)的授课,这是一次美妙的经历,他们当时都在知识系统公司(Knowledge Systems Corporation)工作,这是最早的Smalltalk咨询公司之一。他们给我上的简单一课是这样的:
不要从头开始任何新的编程任务-在开始构建任何新任务之前,请始终先查看Smalltalk映像,看看有哪些可用内容以及已经完成了哪些操作。
对我来说,这是一个启示。它颠覆了我到目前为止所学到的关于编程的一切。但是,当我回想起我的职业生涯和生活时,我意识到这并不是什么不寻常的事情。回想一下你最初是如何学会阅读的(不管是哪种语言)。你的小学老师没想到你一掌握字母就会写小说。不,他们可能做的(我知道我做的)是先给你机会详细阅读,然后你才能写出类似的东西。当我回顾我的小学时代时,我记得我写了很多读书报告-它们实际上只是较大作品的较短摘要-但重点是,我们最终阅读的内容可能是实际写作的10倍。
随着我进入高中和大学,我预计要写的文章、报告和其他东西的长度增加了,但同样粗略的10:1读写比大致保持不变,甚至随着我进入大学,特别是研究生(在那里,这一比例可能是50:1),我的读写比例也在增加。
在这一点上,我的一些Z世代和千禧一代的朋友将失去兴趣,因为他们开始喃喃地谈论婴儿潮一代,原因如下
那只是自吹自擂。我们现在有一个叫做“互联网”的东西,特别是这个叫做“堆栈溢出”的东西。
(我是X世代,谢谢-因此,我天生的X世代愤世嫉俗告诉我,你忘记了我们的存在,不是吗?)。但我想提出的论点是,使用Stack Overflow与我所指的不同,体验也完全不同。当您使用Stack Overflow或像Google这样的搜索引擎时,您所做的就是以您所描述的特定方式缩小解决问题的特定方式。这是因为您是按关键字搜索的-您缺少的是上下文-以及在编写解决方案的上下文中查看解决方案的能力。你还错过了一个偶然发现的思考问题的更好的方法,这可能是你开始搜索时想不到的。您还错过了在源代码中追逐兔子的纯粹乐趣,因为您试图了解代码的某一特定部分是如何工作的,这导致您找到了另一只兔子,又是另一只兔子。
这就是我们回到这个博客的原始主题的地方,那就是“设计模式”一书。当这本书第一次出版时,我已经做了大约五年的Smalltalk程序员-事实上,在这本书正式发行之前,我第一次遇到这些模式是在这本书的一部分的复印预印本中。除了这本书的整体背景之外,它们并没有给我留下太多印象。但是,一旦这本书出版,我就带着一本书从罗利到亚特兰大的长途汽车旅行(大约6个小时),并在路上阅读了它。这一次,它一拍即合。
因此,让我们想想我过去几年的开发方式是如何塑造我的思维的-每当我解决编程问题时,我总是浏览Smalltalk源代码来寻找想法和灵感。这意味着我已经多次阅读了Smalltalk/V和Smalltalk-80(VisualWorks)的大部分(如果不是全部)源代码,并且在我正在工作的项目中一直在使用我从源代码中获得的东西。
设计模式所做的就是突然为我提供了一个词汇表,涵盖了我在阅读所有源代码时已经遇到的许多(不是全部,但很多)东西。我记得在遇到观察者时想--“好吧,我不仅见过这个,而且我已经用了几百次了,因为没有它就不能在VisualWorks Smalltalk中编写GUI。”然后我遇到了Mediator-并意识到我已经在两个Smalltalk版本的GUI源代码中看到了这一点,随着我阅读的越来越多,我不断地体验到同样的似曾相识的感觉,一遍又一遍。
我能想到的最好的比喻是,就好像我一直在读和写小说,但我没有描述我正在做的事情的词汇量-我不知道怎么称呼一个人物,一个场景,或者一个情节装置,但我在自己的作品中都使用了它们,并且在我读过的小说中遇到了它们。
但它真正起到的作用是打开了我的思维,让我认识到这23种模式并不是我见过的所有反复出现的主题。今天,在小说中(尤其是动漫和科幻类型),我们把这些比喻称为比喻,有很多专门针对它们的网站,比如TVTropes,在那里你可以了解到共同的主题,比如疯狂科学家的美丽女儿和邪恶的分类算法(这两个都读了--你会认出它们来的)。但这个想法要古老得多-约瑟夫·坎贝尔(Joseph Campbell)著名地宣称,有一个名为英雄之旅的单一“单一神话”可以捕捉到从“奥德赛”到“星球大战”的每个史诗故事的精髓。
这让我回到了这个博客的原始主题。人们拿起设计模式,希望把它当作文学来读,但你不能这样做--它不是一个故事。你不应该那样读。它更像是一本关于比较文学的教科书。它最好的情况是教你一些关于编写代码的艺术,就像一本比较文学教科书教你一些关于写小说的艺术一样。它不会教你编程-这就是许多人开始应用模式的方式的问题,这导致了令人难以置信的过度使用23个模式,他们在这些模式中应用了“当你有锤子的时候,一切看起来都像钉子”反模式。
相反,您只应将模式视为23个术语,它们可用于描述面向对象编程中最常见的特定比喻。它可以帮助你识别这些比喻,甚至给你一些提示,告诉你什么时候它们可能会有帮助-但最重要的是,它给了你一个与他人谈论这些比喻的词汇表。
现在,还有一件事我想提出--可能是我听到人们对这些模式不屑一顾的最常见的一件事,因为“嗯,这些只是X、Y或Z中的语言特征,所以在那种语言中你不需要它们。”没有什么比真相更离谱的了。
你看,这个特殊的比喻应用到的第一种语言是Smalltalk!当这本书出版时,Smalltalk留言板(记住-这是1996年!)。充斥着来自Smalltalk开发人员的消息,他们说,之所以需要这些模式,只是因为C++(它是当时唯一广泛使用的另一种面向对象语言)非常不足。尽管设计模式中大约一半的示例最初是用Smalltalk编写的,但这仍然是事实。
但这就是我能帮上忙的地方。长话短说,Sherman Alpert、Bobby Woolf和我成为了Design Patterns Smalltalk Companion的合著者--这是Design Pattern的第一本官方续集。我们证明了这些想法和比喻是普遍存在的。您可以用某些语言比其他语言更简单地实现它们,但重要的是,这些思想跨越语言和环境的界限保持不变。
这就引出了我想要说明的最后一点--如果您不理解这些模式,并且不能理解它们,那么也许要做的一件事就是--多读一些代码。如果您主要阅读过自己的代码,而没有广泛阅读过其他人的代码,那么您很有可能没有遇到任何模式,就像您从未看过或读过任何科幻小说或在线玩过MMORPG游戏一样,永远不会遇到邪恶的排序算法。但是,你看的和读的越多,你就越有可能遇到这种比喻。
另一件要做的事情是阅读特定类型的代码-老实说-并不是所有的代码都是平等创建的。在我的工作中,我对客户端代码做了大量的代码审查,主要是用Java,其中一些特别糟糕。事实上,我发现有一个问题是地方性的。这就是代码“假定”面向对象的地方,但实际上,它没有利用面向对象的任何特性(没有继承、没有方法重写,并且很少使用类型替换(如果有的话)。特别是,我看到大量代码中存在这些静态“数据”对象(对Martin Fowler的另一种模式的误解,称为数据访问对象)和这些“处理”对象,它们在很大程度上只是对数据对象进行操作的过程。这只是名义上的面向对象,而不是实践中的面向对象。如果您遇到的唯一代码是这样的,那么我再次理解为什么您可能从未遇到过设计模式中的任何模式。我也为您感到难过,因为像这样阅读代码既痛苦又累人。不幸的是,我认为这种开发风格受到了我们用来开发它们的工具的鼓励-不幸的是,关注文件级别的编辑器永远不会鼓励您考虑文件上下文之外的内容-随着面向文件的语言和开发环境取而代之,另一个东西已经丢失了。
尽管存在这种潜在的痛苦,但在任何情况下,阅读更多的代码都不会是一件坏事。如果您在工作中没有任何好的代码可读,那么我鼓励您转向开源项目-我们中的许多人只是使用开源代码和使用公共API,而没有深入了解内部是如何编写的-这样做将使您有机会看到有多少不同的人编写代码,并比较许多不同的开发风格。即使您没有看到设计模式中的23种模式,您也将学会识别和学习其他的用法,我保证,这些用法将改进您的编程,无论您使用哪种语言进行开发。而且,如果您想用词汇表来帮助其他人描述这些比喻,那么您可以像设计模式的作者那样做-写下比喻,以便其他人可以识别它。这就是每个人都会从中受益的方式--你从别人的经验中学习,其他人在再次遇到这个特定概念时能够采用你的词汇。
顺便说一句,如果您想亲自尝试Smalltalk,Squeak Smalltalk提供了一个非常忠实的Smalltalk开源实现,并且可以在大多数平台上使用。