过去未去,未来已来
同意翻译这本书的时候,我正在自己的琴上练习《云宫迅音》。
对于“云宫迅音”这个名字,也许一般人不是很熟悉,但只要前奏响起,绝大多数人都会第一时间反应过来:啊,《西游记》。
没错,它就是广为人知的“86版”电视剧《西游记》中,给观众留下深刻印象的片头曲。
在很长的时间里,大家都不知道它的名字是“云宫迅音”,甚至作曲者许镜清老师也没有给它正经取个名字。大家印象深刻的是全曲开头电声乐器那特别的旋律,音乐响起,所有人都知道,神魔鬼怪的故事就要上演了。
如今,面对着《云宫迅音》的乐谱,我才发现,这首曲子并非那么简单。仔细分析,全曲涵盖了多个不同的主题,从猴王问世到大闹天宫,从天庭盛会到取经长路,磅礴大气又层次分明,还贯穿着不断升华的主线旋律。在这背后,从调式到节奏,从和声到配器,处处都体现出精妙的味道。
不必观看许老师的采访也可以想见,当年为了这首连名字都没有的片头曲,他一定绞尽了脑汁。最终,这首片头曲留下的影响力,轻松穿透了近40年的光阴。
过去的东西,未必会真的离我们而去,前提是,它是真的好东西。
恰好是在反复练习《云宫迅音》的时候,我收到了翻译这本书的邀请,再三考虑,我决定接下这个任务。
翻译图书,我的顾虑不少。
早年间,我翻译过几本书,也自恃“爱惜名声”,所以对于继续翻译书这回事,我是非常谨慎的,因为技术图书的翻译纯粹是个“费力不讨好”的活儿。
相信大家都知道,如果看完一本英文书的难度是1,那么陈述书中主要观点的难度大概是5,逐字逐句复述书中内容的难度大概是50。如果不但要逐字逐句复述,还必须考虑听者的理解能力,保证听者准确理解,难度还要翻上几番。如果再进一步,要准确还原作者的行文风格,让译文读者产生与原文读者相同的感受,那就难比登天了。
偏生翻译这回事又没有太多工具可以依赖,它有点儿像写代码时,沿着happy path一条道走到黑,既没有单元测试,也没有集成测试,大部分时候全凭手感。所以,译稿出错漏的概率可比代码出bug的概率要高多了。
更麻烦的是,许多读者的技术不错,英语也不错,是故能轻而易举地发现翻译中的错漏。在这个社交媒体流行的年代,任何错误都可能被人拿出来,发在网上四处讨论。换句话说,翻译技术图书这事,风险极大。
所以,该怎么办?还要不要翻译这本书呢?
《云宫迅音》给了我启发。我想,对许老师来说,“会不会流行”“不被人喜欢怎么办”之类的问题,一定不那么重要。相反,用心写好这首曲子,对得起自己,这才重要。
对我来说,阅读英文图书当然没有大的问题;但我仍然要承认,自己读中文书的速度远远超过读英文书。我相信,对许多同行来说,情况也是如此。所以,把这本书的内容以中文形式呈现给大家,会是一件有意义的事情。
加之,这又确实是本特殊的书,与我翻译过的、读过的许多书都不一样。初读这本书的时候,我好几次想要笑出来,因为内容实在太“土”了。
请不要误会,这里的“土”完全没有任何贬义,反而还有赞许的色彩,因为软件开发从来也离不开“土办法”。
我算是计算机专业毕业的,基础知识也不算不扎实,但仍然会犯下“修改防火墙(iptables)配置把自己锁死在外面”的错误。怎么杜绝这种风险,我绞尽脑汁也找不到答案。直到许多年后,朋友告诉我一个“土办法”:先准备一份“绝对没问题”的配置作为备份,再设定一个计划任务,每5分钟就自动载入备份配置,如此就能放心折腾,安全无虞。
这个“土办法”让我拍案叫绝,可是我从没有在教科书里看到过它。我由此想到,许多技术图书都是“阳春白雪”,而“下里巴人”往往不可或缺。那么,类似的经验有没有聚沙成塔、集腋成裘的机会呢?Mark的这本书,恰恰就是我想找的答案。
实际上,Mark也毫不隐瞒,在副标题里直接注明这本书讲的是软件工程中的heuristics。许多人把heuristics翻译为“启发式方法”,其实未必准确,因为它不一定能给人多少“启发”。严格说起来,heuristics指的是一种综合了经验、规则、直觉的试错方法,常用于解决复杂问题,目标不是保证找到最优解,而是在合理的时间内找到接近最优解的实用解决方案。
工作的年头长了,我越来越觉得,软件工程从来不是科学研究,不要求穷尽心力寻找最优解,而是要在合理的时间里找到实用的次优解。而这,恰恰就是“土办法”的价值所在。办法“土”无所谓,能解决问题就是好的。
当然,“土”也会带来疑惑。尤其是在ChatGPT已经诞生并且大肆流行的年代,大量“聪明”的工具涌现出来帮我们生成代码。既然手工写代码都已显得多余,那么手工写代码的各种“讲究”是否还有意义,似乎应该打上一个大大的问号。
但是我继而又发现,许多代码虽然可以自动生成,但最终还是要靠人来阅读、合并、构建的。而不管在中国还是在外国,不管是做哪种系统的开发,也不管从业人员的经验多寡,软件工程的许多问题都是共通的。
比如,与代码渐行渐远的文档;比如,大杂烩式的提交(commit)操作;又比如,含混潦草的提交说明(commit message)…… 所有这些,都会让你在与人合作的时候无比头痛,哪怕现成的代码库摆在眼前,也不过是《西游记》中的盘丝洞,看一眼就要倒吸一口凉气。
每当这种时候,我都想让当事人去读读这本书,好好补习基本功。
又比如,在多团队协作联调时,一个诡异的bug消耗了我们的大量时间,最后发现对接口的调用没有错,可惜接口的设计有误,调用某个接口之后,对象就处于异常状态。然而当我指出问题之后,合作团队的工程师轻描淡写地说:“噢,问题不大,你再调用某某接口,状态就会对了。”
那一刻,我恨不得直接把他从屏幕那面拽过来,指着这本书上的内容给他看:“从本质上讲,封装应该保证一个对象永远不会处于无效状态。”虽然他确实是一位资深工程师,但他无疑还应当补习这些基本常识。
当然,阅读这本书的过程,也不仅仅是强化已有常识的过程。实话实说,我也收获了很多新的知识。
比如“提交记录分析法”。在新接触已有代码库时,先对过往提交记录做个统计分析,画出提交的热点围栏图,就可以很清楚地知道大家的工作模式,哪些模块是当前的重点,bug主要集中在哪些模块—这个办法的确比自己闷头读代码快得多。
再比如“橡皮鸭法”。在面对问题一筹莫展时,找个橡皮鸭玩具,或者想象自己正对着橡皮鸭,尝试复述问题和自己的思路。“你看,现在我遇到了这个问题,它的现象是这样的……我尝试分析,第一步我做了……第二步我做了……那么接下来我大概应该……”。
看起来有点儿滑稽,但它真的帮我排除了不少疑难杂症。
Mark还告诉我,如果实在没有思路,也不要紧。一定要站起身来,离开计算机,四处走动走动,干点儿不相干的事情。切记不要沉迷在问题当中,而是把它交给“系统1”(也就是潜意识)去思考。经过一段时间的“发酵”,没准儿就能水到渠成。
看起来有点儿荒谬,但我真的很多次在骑自行车时体验了灵感爆发。
因此,“土”归“土”,我还是很高兴地读完了这本书,也愿意让更多人读到这本书。要知道,软件开发这回事,从来不是在理论、工具、框架的新世界里叱咤风云,也从来不能对留传下来的宝贵实践经验嗤之以鼻。
怎样提交代码,怎样写提交说明,怎样设计API来杜绝用户做你不想让他们做的事情,怎样迅速理解巨大的代码库,怎样找到排障的思路……经典的教科书完全不会涉及这些问题,在那些书中,写提交说明、提交代码、设计接口、定位故障……似乎这一切都会按部就班地自然发生。
然而在真实的开发中,我们又常常被这些“不起眼”的问题困住,消耗了太多的时间和精力。更可惜的是,如果没有人告诉开发人员怎么做才是对的,那么他可能根本意识不到,这类问题本不应该浪费如此多的时间和精力。
所以,阅读这本书,也暗合了Mark反复提起的那句话:“未来已来,只不过是参差而来。”
“未来已来”,让读者意识到旧问题,寻找到新答案,提升自己的技艺水准。至于“参差而来”,则意味着读者不必完全赞同Mark的每个观点—老实说,他的许多观点我看了也皱眉,比如他对ORM的反感,以及他对于计算机科学知识价值的低估,无论是根据理论认知还是实践经验,我都无法赞同他的这些观点。所以,建议大家因地制宜,放心拿走对自己有用、有启发的观点。
要补充的是,Mark的这本书,行文是非常轻松随意的,因此即便是相对严肃的话题,你也看不到一本正经的腔调。虽然中英文的表达方式有很大不同,但我仍然努力保持、还原英文原书那种轻松随意的风格,希望大家在收获知识的同时,也有良好的阅读体验相伴。
我衷心地希望,在未来的某个时刻,也许你早已忘记了作者和书名,可是一看到模糊潦草的提交说明,就能条件反射般地知道:嘿,这可不行! ——余晟 2024年3月
序
在2005年左右,我开始为一家出版社做技术审校。审校过若干本书之后,编辑要跟我谈一本关于依赖注入的书。
这样的开头有点儿奇怪。通常,他们跟我谈起某本书的时候,作者和大纲都已经有了,可是这次什么也没有。编辑要给我打个电话,聊聊这样的主题是否行得通。
我仔细想了几天,觉得这个主题有意思。但是同时,我觉得不值得为此写一整本书。毕竟,这些知识都是现成的,博客、库文档、杂志文章,甚至有些书已经覆盖了这类主题。
反思之后我意识到,尽管知识是现成的,但是它们是零散的,使用起来也没有一致性,有些术语甚至是互相矛盾的。所以,把这些知识收集起来,以规范一致的模式语言展现出来,会有很大的价值。
两年之后,我写的书出版了,这让我自豪。
又过了些年,我开始考虑再写一本书,并不是你手上的这本,而是关于其他主题的书。然后我有了第三个、第四个想法,但都不是你手上的这本。
10年之后我逐渐意识到,在教导软件开发团队如何写出更好代码的时候,我会推荐采取一些做法,这些做法都来源于比我更聪明的头脑。我也意识到,大多数的知识已经是现成的,但是散落四方,几乎没有人明确地把零散的知识点串起来,熔铸为关于软件开发的一整套描述。
基于写作的经验,我知道做这件事是有价值的,应当把散落的信息收集起来,以逻辑一致的方式呈现出来。我尝试这么做了,成果就是你面前的这本书。
谁应该阅读本书
本书的目标读者,是已经有若干年专业经验的程序员。我期望读者经受过糟糕的软件开发项目的折磨,也见识过难以维护的代码。我还希望读者是希望进步的人。
本书的核心目标群体应当是“企业应用开发者”—尤其是后端开发人员。我职业生涯的大部分时间都在这个领域,所以这能够反映我的专业性。但如果你是前端开发人员,或者是游戏开发人员、开发工具工程师,抑或是完全不相干领域的人,我仍然希望本书能让你大有收获。
你应该能够很舒服地阅读C语言家族中的编译型、面向对象型语言。虽然我职业生涯中的大部分时候使用的是C#语言,但我也从以C++或Java为示范语言的图书中学到了很多。本书反其道而行之,示例代码基于C#语言,但是我希望Java、TypeScript、C++开发人员也能有所收获。
前提要求
这不是一本入门书。虽然它讲的是如何组织和结构化源代码,但不包括基础的细节。我希望你已经明白一些入门知识,比如,缩进有什么好处,冗长的方法为什么不好,全局变量是糟糕的。我并不期望你读过《代码大全》[65],但是我假设,你应当掌握了该书所涉及的一些基础知识。
给软件架构师的建议
即便局限在软件开发的场景中,对不同的人来说,“架构师”的含义也是不同的。有些架构师关注宏观图景,努力帮助整个组织取得成功。其他架构师则深入代码,主要关注特定代码库的可持续性。
从这个意义上来说,我是后一种类型的软件架构师。我的专长在于,我懂得如何组织源代码,达成长期商业目标。我写下自己的经验所得,所以本书对我这一类架构师是很有用的。
本书没有涵盖关于Architecture Tradeoff Analysis Method(ATAM)、Failure Mode and Effects Analysis(FMEA)、服务发现之类的内容。这些主题不在本书的介绍范围之内。
内容结构
本书谈的是方法论,我会围绕一套贯穿全书的示例代码库来组织内容。之所以这么做,是为了让读者的体验胜过阅读常见的“模式目录”。这个决定的结果之一是,我会引入符合“叙事”节奏的具体实践和实用方法。在指导团队时,我也是这么做的。
本书的叙事结构围绕着一个示例代码库展开,它实现了一个餐厅预订系统。你也可以从网址链接1下载示例项目的源代码。
如果你希望把本书当成手册,我提供了一份附录,按照其中的清单,你可以找到所有的实践步骤和详细信息,然后深入阅读对应的章节。
代码风格
我的示例代码是用C#语言编写的,近年来它进化得很快。它从函数式编程中获得了越来越多的新语法,比如,不可变记录类型(immutable record type)在我写作本书时已经发布。不过,我决定忽略某些新的语言特性。
曾经有一段时间,Java代码和C#代码看起来很相似。然而,现代的C#代码看起来并不像Java代码了。
我希望这些代码可以被尽可能多的读者理解。我曾经从以Java为示例语言的图书中获益良多,所以我也希望,读者无须了解新的C#语法,就能读懂本书。因此,我尝试坚持只使用C#的一小部分特性,其他语言的程序员也应当能理解。
这并不会影响书中讲解的概念。没错,在某些时候,C#有自己专属的更简洁的办法,但这可不是坏事,意味着读者可以进一步优化书里的这些代码。
用不用var,这是个问题
C#在2007年引入了var关键字。这样在声明变量的时候就不必显式指定其类型,编译器会根据上下文来做出推断。简而言之,var类型的变量和明确指定的静态类型的变量没有区别。
长期以来,使用这个关键字是有争议的。不过现在大多数人都在使用它,我也是如此,虽然有时候我会遇到一些阻力。
虽然正经干活儿时我会用var,但是,为写书提供示范代码与此略有不同。在正常情况下,IDE(集成开发环境)是标配。现代的IDE能够迅速显示出推断的类型,但是图书不会。
所以,我有时候还是会显式指定变量类型。本书中的多数示例代码仍然使用var关键字,因为这样代码会更短,且行宽也受制于书页。不过也有些时候,我会显式声明变量类型,以使读者更易于理解代码。