程序员的制胜技
上QQ阅读APP看书,第一时间看更新

1.4 现代软件开发存在的问题

除了不断增加的复杂性,抽象层次越堆越高,Stack Overflow渐渐失了水准之外,现代软件开发还存在着以下这些问题。

技术繁多:如此之多的编程语言,不可胜数的框架,层出不穷的库。npm(Node.js框架的包管理软件)中甚至有一个名字叫“left-pad”的库,其作用仅仅是给字符串末尾添加空白字符。

现代软件开发由范式驱动:这导致了软件开发的保守主义。在很多程序员眼里,编程语言、最佳实践、设计模式、算法和数据结构好像远古异族遗迹一样神秘,摸不着其中的原理。 

科技黑箱:就像汽车,之前人们还可以靠自己来修理,如今发动机越来越高级,引擎的金属盖如同盖在法老坟墓之上,谁要打开它,就会被诅咒。软件开发如出一辙。虽然现在几乎所有的东西都是开源的,但是20世纪90年代之后,软件的复杂性成倍增长,我觉得如今的技术比二进制代码的逆向工程还要难懂。

代码开销:我们拥有了远超所需的资源,人们不再关心代码开销。现在你还需要写一个简单的聊天软件吗?把它集成到成熟的浏览器框架里会比较省事,而且没人会为了软件消耗以吉字节计的内存而心疼,何乐而不为呢?

自扫门前雪:程序员只关注自己的技术栈,对支撑这些技术栈的知识漠不关心。这也情有可原:饭都吃不上,哪有时间去学习呢?我把这叫作“开发者的吃饭难题”。也正是由于受自身知识所限,那些会影响他们产品的因素同样被忽略了。网页开发者通常对网页底层网络协议一头雾水,只能忍受网页加载延迟。因为他们不知道其中的技术细节,比如说不必要的长证书链会拖慢网页的加载速度。

憎恶重复:多亏了人家教给我们的范式,大家对那些无需技巧的工作(比如重复工作或复制、粘贴)毫无热情。你必须得找到DRY[7]的解决方案。这种观念会让你怀疑自己和自己的能力,从而影响你的生产力。


[7] 即don’t repeat yourself的缩写。这是一种迷信,好像如果有人重复写某段代码,而不是把它封装到一个函数里面,这个人就会立马变成一只青蛙。

npm和left-pad的故事

在过去的10年里,npm成了事实上的JavaScript包生态系统。人们可以向生态系统贡献自己的包,其他的包也可以使用它们,这降低了开发大型项目的难度。阿泽尔· 科丘卢(Azer Koçulu)就是开发者之一。left-pad是他为npm生态系统贡献的250个软件包中的一个。它只有一个功能:将空格附加到字符串上,以确保字符串的大小始终是固定的。这个操作轻而易举。

有一天,他收到npm的一封电子邮件,说他们已经删除了他的一个名为kik的包,因为他们收到了一家同名公司的投诉。npm决定删除阿泽尔的包,并将名称的使用权交给那家公司。这惹怒了阿泽尔,一气之下他删除了他贡献的所有包,包括left-pad。但问题是,前文也提到了,世界有数百个大型项目直接或间接地使用了这个包。他的这个行为导致受波及的所有项目都停止了。这是一场大灾难,也让人对平台的信任打了问号。

我讲这个故事的意思是,软件开发世界中充满了你不愿意看到的意外。

在本书里,我对这些问题给出了解决方案,也会详细讨论一些你也许会认为无聊的核心概念。我还会推荐你优先考虑实用性和简洁性,勇敢地拒绝一些长期以来的“金科玉律”,更重要的是,凡事要质疑、反思,这是有价值的。

1.4.1 技术繁多

由于“银弹”谬论,我们总在不断追求最佳技术。我们认为总有一种技术可以将生产力提高好几个数量级。但是,当然没有。打个比方,Python是一种解释型语言,不需要编译Python代码,就可以立即运行。更方便的一点是,Python[8]中不需要为声明的变量指定类型,这使得写代码更快。那么,Python比C#更好吗?不一定。


[8] Python是那些想推广使用空格缩进的人包装出来的实用编程语言。

因为你不用花时间进行类型声明来注释代码和编译,对代码当中出现的问题就会直接错过了。这就是说你只能在测试时或上线到生产环境中才发现它们,这要比顺手编译付出的代价高得多。大多数技术都是权衡利弊,找出折中的方案,而不是生产力的助推火箭。提高效率靠的是你对这项技术的熟悉程度和应用技巧,而不是你使用的技术本身。当然,有更好的技术,但它们很少能带来数量级上的效率提升。

1999年,我正想着着手开发我的第一个交互式网站,那时我完全不知道如何编写一个Web应用程序。按常理,我应该首先尝试找到最好的技术,那就意味着我要自学VB Script或Perl。相反,我使用了当时我最熟悉的语言:Pascal。这是被认为最不适合Web应用开发的语言之一,但我用它成功运行了Web应用程序。当然,它肯定也有很多问题,比如每次这个应用程序挂起时,这个进程依然会停留在加拿大机房的某台服务器的内存里。然后用户就得给服务提供商打电话,让他们重启那台物理服务器。总而言之,Pascal让我在短时间内就搭好了原型,我对它很满意。你看,我并没有花几个月的时间去学习然后再开发我预想的网站,而是仅仅花了不到三个小时的时间就编程完成并发布代码。

在后文中,我会介绍一些让你能更高效地利用你所拥有的工具的方法。

1.4.2 遍阅范式

我最早接触到的编程范式是20世纪80年代的面向过程编程,结构化编程里没有goto,没有血汗,也没有悔恨的泪水,而是由结构化的代码块(比如函数或者循环)组成的。这种设计让代码更容易维护和阅读,同时还不牺牲性能。也是因为结构化编程,我开始对Pascal和C语言产生了兴趣。

大约过了5年,我又接触到另一种编程范式——面向对象编程,或者叫作OOP(object-oriented programming)。我记得那个时候,数不清的计算机相关杂志铺天盖地地宣传着它。在用面向过程编程之后,怎么样把程序写得更好,变成了一件大事。

在OOP之后,我以为至少要每过5年才能出现一种新范式,没承想,新范式的出现比这还要频繁。在20世纪90年代,Java和JavaScript网络脚本与函数式编程出现了,即时编译(just-in-time compilation,JIT compilation)[9]随之成了主流。截至90年代末,函数式编程慢慢进入了主流编程领域。


[9] 即时编译,Java的创建者Sun微系统公司(现已被收购)创造的一项“神技”。其作用是代码在运行的同时进行编译,整个程序的运行速度会变得更快,因为优化器将在运行时收集更多的数据。虽然我能够“解释”它,但这并不妨碍它是一项神技。

时间来到千禧年,在这几十年里,多层架构的应用(n-tier application)越来越多,如胖客户端,瘦客户端,泛型,MVC、MWM和MVP。随着可信、面向未来及最终的响应式编程(reactive programming)出现,异步编程也开始迅猛发展。类似LINQ那样拥有模式匹配和不变性概念的函数式编程语言进入主流语言之列,风靡一时的新词层出不穷。

讲了这么多我们还没有谈到设计模式和最佳实践。如今,几乎所有项目都有多如牛毛的最佳实践、技巧和窍门。虽然问题的答案显而易见(是空格键),但还是有很多与我们应该用空格键或是Tab键有关的表达观点[10]


[10] 我从实用的角度讨论了这两者,读者可以搜索tabs-vs-spaces-towards-a-better-bike来进行阅读。

我们设定所有问题都可以通过采用某一种范式、某一种模式、某一种框架或是某一种库来解决。但考虑到我们所面临问题的复杂度,这种美事是不存在的。不仅如此,盲目地使用某种工具只会给以后埋下隐患:需要学习全新领域的知识,而且这些新玩意儿本身也有一大堆缺陷。你的开发速度会更慢,甚至倒逼你在设计上进行相应的妥协。本书会让你对使用模式更加有信心,更加感兴趣,在代码复查(code review)时尝到甜头。

1.4.3 科技黑箱

框架,或者一个库,就像一个安装包,软件开发者可以安装它,阅读相关文档,然后运行它。但软件开发者通常不清楚其工作原理,算法和数据结构对于他们来说也是一样的。因为键值对的形式易于使用,所以他们就采用字典数据格式,但他们不知道这么做会带来什么后果。

无条件信任某个包及其生态或者各类框架是很容易出现大问题的。如果不知道往字典中添加具有相同键的项在查询时与列表性能一样的话,可能就会花上几天的时间去调试。当用一个简单的数组就可以满足需求时,我们又采用C#生成器,由此带来的性能下降,我们还可能找不到原因。

1993年的某天,朋友递给我一张声卡,让我装在计算机里。对,为了有个过得去的音效,我们曾经还得给计算机装一张声卡,不然你除了哔哔声之外什么都听不到。言归正传,我之前可从来没有打开过我的计算机,我怕把它拆坏了。我问朋友:“你不能帮我装一下吗?”他答复:“你得亲自打开它,才会明白它是怎么运行的。”

我很认同他这句话,我懂了,我的焦虑是因为我的不了解,而不是因为我的不能够。我打开机箱,看到机箱内部,然后就释然了,这就是一堆板子而已嘛。这块声卡放在……嗯,放在这个插槽里。从此这玩意儿对我来说再也不是一无所知了。过后,在给艺校的学生讲计算机基础知识的时候,我又用了类似的技巧。我拆开一个鼠标的轨迹球,并给学生们看。顺便提一句,那个时候实验室的鼠标还有个球。唉,这样说容易引起误解。[11]然后我又打开机箱,说:“你们看,这也没什么,就是一些板卡和插槽嘛。”


[11] 在这里,“balls”既可以指鼠标内部的滚珠(一种较早的鼠标设计),也可以指代睾丸。——译者注

从此以后,这也成了我处理所有没接触过的复杂问题的准则。我对于一开始就“打开盒子”,见识到事情的全貌,会感到不过如此。

类似地,了解某个库、某个框架,或者一台计算机的工作原理,对你去理解以它们为基础的东西有很大帮助。勇于“开盒”,了解其中的细节,对于你怎样使用“盒子”也是一样的。你真的没有必要阅读全部代码,或者浏览厚达千页的理论书,但至少应当明白某部分的功用,以及它如何影响你的项目。

这就是我在后文会谈到一些基础或者说底层原理的原因。“打开盒子”,好好瞧瞧,这会帮助我们找到高层级编程的更佳选择。

1.4.4 低估开销

我其实非常愿意看到每天有这么多基于云的应用程序,因为这种方案不仅性价比很高,而且方便清晰了解代码的实际开销。当你因写代码时的决策错误而为产生的云服务器额外费用买单的时候,你肯定立马就会把开销当回事了。

框架和库是有用的抽象,通常能帮我们减少开销。但是你不能把所有决策都推给框架来定。有的时候,我们必须靠自己做抉择,必须考虑程序开销问题。开销这一因素对大体量的应用程序更重要。哪怕节省很少的时间,都可能帮你省下宝贵的资源。

一个软件开发者首先要考虑的当然不是开销,但是,至少在某些情况下要懂得如何去避免开销。梳理这种观念,会帮助你节省时间。这不光是为了你自己,也是为了那些眼巴巴看着你开发的网页载入提示“转圈”[12]的用户们。


[12] “转圈”当属现代的沙漏。在早期,电脑用一个沙漏符号来让你无限等待加载。“转圈”是现代动画的沙漏等价物。它通常是一个无限旋转的圆,不过它的作用只是分散用户的注意力。

在本书中,你会看到我给出的一些场景和例子,这些场景和例子会告诉你如何轻松避免开销。

1.4.5 自扫门前雪

只关注我们负责的东西:我们拥有的组件、我们编写的代码、我们造成的缺陷,还有在办公室厨房微波炉里偶尔烤糊了的午餐,这是我们处理复杂问题的一种方法。这听上去确实是节省了我们的时间。但是别忘了,所有的代码都是息息相关的。

了解特定技术、库的工作原理,依赖关系是如何工作和连接的,可以让我们在写代码的时候做出更好的决策。本书给出的例子能够给你提供一个新的视角,让你不再只关心自己那一亩三分地,而接触到你舒适区以外的那些相关知识和难题。因为这样,能让你对自己所写代码的命运做到心中有数。

1.4.6 憎恶重复

所有关于软件开发的原则,总结成一句话就是:你的工作时间越少越好,避免重复的、无脑的工作,比如复制、粘贴,或是为了做一些小更改而从头编写只有少部分不同的代码。首先,它们的确很花时间,其次,它们的可维护性非常差。

不过,不是所有的“打杂”都没用。复制、粘贴有时是有用的,虽然很多人对它有很深的成见,但是在某些方面,将它们运用好了,甚至会比你之前学到的最佳实践更加有用。

除此以外,并不是你写的所有代码都会被加入实际产品中。你写的代码中有些可能会被用于开发某些还处于雏形的产品,有些比较适合做代码测试,有些又可以作为你手头上事情的错误范例来警示自己。我会在后文举一些例子,介绍如何用这些方法给自己带来好处。