抱怨 JS 疲劳就像是在抱怨人类发明了太多解决问题的工具:从邮件到飞机到宇宙飞船。
上周我在 NebraskaJS 2017 会议上做了一个和这个话题极其类似的演讲,我也收到了许多积极的反馈,所以我就想这个演讲也可以写成一篇文章发表出来,让更多的人知道,并帮助他们应对 JS 疲劳,理解我们行业的真相。这篇文章的目的是希望改变你对软件工程行业的普遍的看法,助你在你可能工作的领域上一臂之力。
激励我写下这篇文章并且彻底改变我生活的一个原因是 Patrick McKenzie 写过一篇很帮的文章:《请不要自称程序员,我十多年的 IT 职场总结》。强烈推荐大家先阅读前面这篇文章。本文的大部分内容都是基于 Patrick 的那篇关于 JavaScript 生态系统的文章的建议,其中也夹杂了最近几年我在科技行业工作的一些想法。
第一个章节可能会有点哲学化,但是我保证绝对值得一读。
我们行业的真相 101
就像 Patrick 在 他的文章 里写到的,我们先从一些最基础、最根本的真相说起:
软件是用来解决业务问题的
实事就是这样。软件存在的意义并不是用来取悦我们程序员的,并不是为了让我们写出漂亮代码的,也不是为科技行业创造就业机会的。实际上,软件的存在扼杀了太多的工作岗位,其中也包括我们的,这就是为什么基本工资在未来的几年将会变得更加重要,但是这就完全是另一个话题了。
我这样说很抱歉,但是归根结底原因在于:在软件工程中(其他行业也是如此)只有两样东西至关重要:
支出和收益
支出削减的越多,收益提升的越多,那么你的价值就越大,削减支出、提升收益最通用的一个方法就是用机器代替人工,从长远来看,这种方法是更有效的,而且支出也更少。
你不是被雇佣来写代码的
科技不是目的。没有人关心你使用的是什么编程语言,没有人关心你的团队选择的是什么框架,没有人关心你的数据结构有多么优雅,也没有人关心你的代码写得有多漂亮。人们唯一关心的是:你的软件支出是多少,产生的收益是多少,仅此而已。
写出漂亮的代码对于你的客户而言没有任何卵用。我们之所以要写漂亮的代码,是因为长远来看这样会更高效,能够减少支出,增加收益。
我们努力避免 bug 不是说我们重视正确性,而是我们的客户重视正确性。如果你曾经遇到过一个 bug 成为了一个特性的话,那么你就知道我在说什么了。这个 bug 确实存在,但是我们不会去修复。出现这种情况并不是说我们的目的就是制造 bug,我们的目的是创造价值。但是如果我们的 bug 能够使客户开心,能够提升他们的收益,我们也能达成我们的目标,如此皆大欢喜,何乐而不为呢。
可重复使用的太空火箭、自动驾驶汽车、机器人、AI:这些东西之所以存在,并不是因为我们觉得它们很酷,而是因为在它们的背后有商业利益存在。我并不是说这些东西背后的那群人只在乎金钱,我确定他们也认为这东西很酷,但事实是,如果没有经济利益或者说没有潜在的经济价值,这些东西是不会存在的。
也许我不应该将这一章节取名为“我们行业的真相 101”,也许应该取名为“资本主义真相 101”。
说到我们的目标——减少支出提升收益——我认为作为程序员的我们应该更加关注需求和设计,积极思考,积极参与业务决策,这就是为什么了解我们正在开展的问题领域变得极其重要。之前你有多少次在你的经理或商务人士没有考虑到的某些边缘案例中想到应该发生什么?
1975 年,Boehm 做过一项研究,研究发现,软件中 64% 的错误都是由设计引起的,只有 36% 的错误是代码错误。另一项叫做 “高阶软件——软件定义方法论” 的研究也显示:在 NASA 的阿波罗计划中,73% 的错误都是设计错误。
设计和需求存在的唯一目的就是定义我们将要解决的问题,而解决问题就能创造收益。
没有了需求或设计,编程就是往一个空的 text 文件里面添加 bug。—— Louis Srygley
这个原则同样适用于 JavaScript 生态系统的工具。Babel、webpack、react、Redux、Mocha、Chai、Typescript,所有这些工具之所以存在就是为了解决对应的问题,我们要理解它们要解决的是什么问题,要仔细思考什么时候需要这些工具,否则,我们就会感到 JS 疲劳,因为:
当我们使用我们不需要的工具来解决根本就不存在的问题的时候,JS 疲劳就出现了。
正如 Donald Knuth 曾今说的:“过早优化是万恶之源”。请记着,软件的存在是为了解决相应的业务问题,大部分的软件其实都挺令人厌烦的,既没有多强的扩展性,也没有高性能约束。要专注于解决业务问题,专注于减少支出、提升收益,这才是需要关注的焦点。只有在当你需要优化的时候才去优化,否则,软件可能会增加一些不必要的复杂性,而这些复杂性会增加支出,并且不能产生足够的收益来抵消这些支出。
这就是为什么我认为应该在我们的工作当中应用 测试驱动开发 原则。我说的测试驱动开发并不是说仅仅去做测试。我说的是在问题暴露之前将其扼杀在摇篮里。这才是 TDD 要做的。正如 Kent Beck 说的“TDD 减少了恐惧”,因为它能够指导你的开发节奏,允许你慢慢地逐步解决你的问题,一步一个脚印,一次解决一个问题。当我们要使用新的技术时,这样做同样也会减少恐惧。
一次解决一个问题同时也降低了 分析麻痹,举个栗子,就好比你打开了 Netflix,你本可以看一些视频的,但是却花了三个小时来决定看什么。一次解决一个问题的方式可以缩小我们做决定的范围,缩小了做决定的范围我们的选择就会相对减少,选择减少了我们就降低了分析麻痹。
不知道你有木有想过,如果只有几个可看的电视频道,决定看哪个频道会变得多么简单?如果家里只有几张游戏盘,决定玩儿哪个游戏会变得多么简单?
那么对于 JavaScript 而言呢?
截止到我写这篇文章时,NPM 上有 489,989 个包,第二天将会有差不多 515 个包在上面发布。
我们使用、抱怨的这些包都是有一个历史出发点的的,为了理解我们为什么需要这些包,我们必须理解这个历史出发点:它们是用来解决问题的。
Babel、Dart、CoffeeScript 和其他转义器之所以会出现,是因为我们不仅仅使用 JavaScript 写代码,但是我们又想使其能够在浏览器中正常运行。Babel 甚至能够使我们使用 JavaScript 新版本语法写的代码在旧版本浏览器中运行,因为众所周知,不同版本的 ECMA 规范在各个浏览器中的兼容是一个很大的问题。尽管现在 ECMA 规范已经越来越可靠,但是我们仍然需要 Babel。如果你想了解更多关于 Babel 的相关知识,我强烈推荐你读读 这篇由 Henry Zhu 写的很赞的文章。
像 Webpack 和 Browserify 这样的模块化打包工具也有它们存在的理由。想必你们依然记得,曾几何时,我们使用大量的 script 标签将脚本引入使其能够正常运行。这样做的结果就是污染了全局命名空间,当一个脚本依赖另一个脚本时,很难合理地将它们整合起来。为了解决这个问题,Require.js诞生了,但是它仍然有它自己的问题:它不够简单,语法也会引起其他问题,正如你在这篇文章中看到的。然后 Node.js 借鉴了 CommonJS 的 import,这种 import 是同步的,简单整洁,但是我们仍然需要一种可以在浏览器中运行的方式,这就是为什么我们需要 Webpack 和 Browserify 的原因。
Webpack 确实解决了很多问题,比如可以像处理 JavaScript 依赖那样处理 CSS、图片和许多其他的资源。
前端框架确实有点复杂,但是由于它们的存在,使得我们写代码时减少了同步加载,如此一来,我们就不必担心 DOM 操作,甚至也不用和那些乱七八糟的浏览器 API(JQuery 已经解决了这个问题)直接打交道,众所周知,浏览器的兼容性处理错误百出,而且效率低下。
这就是我们在计算机科学中一直在做的事情。我们使用低级抽象,并在其上构建更多的抽象。我们应该更多考虑的是,我们的软件应该如何运行,而不是怎么让它运行,这样的话,才能更高效。
但是所有这些工具都有一个共同之处:它们之所以存在是因为 web 平台发展太快了。如今任何地方都有 web 技术的存在:web 浏览器,桌面应用,电话应用,甚至手表应用。
这个革命性发展同样也暴露出了我们需要解决的问题。比如,渐进式 web 应用(PWA),它们之所以存在不是因为它们很炫酷,不是因为作为程序员的我们乐于写 PWA。请牢记本文的第一节:PWA 之所以存在,是因为它们创造了商业价值。
通常情况下,标准的制定速度并没有那么快,因此,针对对应的问题我们需要自己寻求解决方法,这就是为什么有一个活跃度高、有创造力的社区是一件很 nice 的事情。我们不是在解决问题,就是在去解决问题的路上。当然,我们也会顺其自然。
适用于我们的工具会更好地成长,获得更多的贡献者,更快地发展,有时一些工具最终将融合来自于其他工具的优秀想法,并且变得比它们更受欢迎。这就是我们演变的进程。
拥有越多的工具,我们就会拥有越多的选择。想必你还记着 UNIX 思想,它主张我们在编程时,一次只做一件事情,而且要将它做到极致。
我们可以清晰的看到这种思想在 JS 测试环境中重现,比如,我们使用 Mocha 跑测试,使用 Chai 做断言,而在 Java 中,JUnit 把这些事情全部包揽了。这就意味着,如果你在使用某一个工具的过程中遇到了问题,并且发现另一个工具更适合我们的话,那么我们就可以直接简单的替换掉之前的工具就可以了,其他工具的优势我们依然能够保留。
UNIX 思想同时主张我们应该去写能够“和谐相处”的程序。这正是我们在做的!比如说 Babel、Webpack 和 React。同时使用它们完全能够正常运行,但是我们并不需要使用一个工具而去依赖另一个工具。比如我们在测试环境中使用 Mocha 和 Chai,那么我们也可以安装 Karma 在多种环境中来跑同样的测试。
如何应对?
针对正在遭受 JS 疲劳的同学,我的第一个建议是:你要清醒的认识到你并不需要掌握所有东西。有时我们会一次性学习过多的知识——甚至当我们并不需要的时候,这样做只会增加疲劳感。你喜欢的领域你要保持积极的学习动力,可以深入了解,而对于其他的知识,你大可保持慵懒的态度。我这里说的慵懒不是让你懒惰,而是在你需要某些知识的时候再去学习。当你遇到了问题,且需要使用某项知识技能来解决的时候,直接现学就可以了。
另一个重要的建议是:脚踏实地,从头开始。在你使用任何 JavaScript 框架之前,请确保你对原生 JavaScript 学习的已经足够透彻。这是你掌握 JavaScript 并能够将其“玩弄于鼓掌之中”的唯一途径,否则,当你遇到了你之前从来没有见过的问题时,你就不知道该如何下手。学习核心的 web 技术——CSS、HTML5、JavaScript和计算机科学基础,甚至是 HTTP 协议的工作原理——将会有助于你快速掌握任何其他的技术。
但是,请务必不要用力过度。偶尔你要挑战一下自己,亲自动手做一些项目。正如 Sacha Greif 在这篇文章里写到的那样:花费过多的时间学习基础知识就像你要学习游泳时总是在学习流体动力学。学到一定程度以后,你就应该跳到游泳池里去尝试游泳。
同时,请务必不要拘泥于一项技能。我们现在可用的技术其实在过去都早已被发明出来了。当然,它们特性不同,名字不同,但是,本质上它们都是相同的。
如果你看看 NPM 的话,它也不是什么新技术,很早之前就有 Maven Central 和 Ruby Gems 了。
用来转义代码的 Babel,也是借鉴了早期一些非常出名的编译器的原则和理论,比如 GCC。
甚至 JSX 也不是什么新想法。E4X(ECMAScript for XML)10 多年前就存在了。
那么现在你可能会问:“那 Gulp、Grunt 和 NPM 脚本又如何呢?”额,好吧,很遗憾的告诉你,这些问题 GNU Make 在 1976 年都已经解决了。实际上,现在仍然有大量的 JavaScript 工程在使用 GNU Make,比如 Chai.js。但是我们不会那样做,因为我们是喜欢复古的潮人。我们使用 make,因为它解决了我们的问题,正如我们之前讨论过,这就是你的目标所在。
如果你真的想要理解某项技术,想要在面对任何问题时都能够得心应手,那么,请深入了解。成功最关键的一个因素就是好奇心,所以请深入了解你喜欢的技术。尝试自下而上的理解它们,每当你认为某些东西如“魔法”一般时,那么请通过探索代码库来揭开它的神秘面纱。
在我看来,说到学习这块儿,Richard Feinman 的这句名言最适合不过了:
我创造不了的东西,我理解不了
再来看看下面这句,这是 Richard 在同样的一块儿黑板上写下的:
对于已经存在答案的问题要知道如何解决
是不是很赞?
当 Richard 说这些话的时候,他正在讨论的是关于获取理论结果并如何复现的问题,但是我认为,该原则同样适用于软件工程。能够解决我们的问题的这些工具已经被发明出来了,它们已经存在了,所以我们也应该能够自己来实现它们。
这正是我喜欢 Egghead.io 中一些视频的原因,视频中 Dan Abramov 解释了如何从头开始实现 Redux 中存在的某些功能,或者教你如何构建自己的 JSX 渲染器。
那么我们为什么不去尝试着自己来实现或者去 GitHub 上阅读代码库理解它们的原理来实现这些东西呢?我确定你一定能够发现很多有用的知识。评论和 demo 也许会撒谎,也许会误导,但是代码不会。
另一个我们在这篇文章中谈论的最多的话题是:你不应该超前你自己。遵循 TDD 原则,一次只解决一个问题。你被雇佣是来降低成本提升收益的,你所做的都是为了解决问题,这就是软件存在的意义。
既然我们喜欢拿我们自身的角色和土木工程相关的作对比,那么就让我们快速对比下软件开发和土木工程,就像Sam Newman 在《构建微服》中做的那样。
我们喜欢自称“工程师”或“架构师”,但是这样真的好么?我们一直在为不到一百年前的计算机开发我们所知的计算机软件,而竞技场都存在大约两千年了。
还记得最近一次看到一座桥坍塌是什么时候吗?还记得最近一次你的手机或浏览器奔溃是什么时候吗?
为了更好的解释,我借用一个我比较喜欢的栗子。
这是美丽惊艳的巴塞罗那。
当我们从这个距离看这座城市的话,它开起来和世界上的其他城市一般无二,但是当我们从上面俯瞰时,巴塞罗那看起来是这个样子的:
正如你看到的,每一个块儿都有着相同的尺寸,所有的块儿都有条不紊的排列着。如果你曾经去过巴塞罗那的话,你一定知道穿越这座城市有多么爽,知道它运行的多么良好。
但是坐在飞机上俯瞰巴塞罗那的人,无法预知两百年或者三百年后它会成为什么样子。城市里的人们进进出出,他们要做的是让城市随着时间的流逝有机地成长、适应。他们必须做好应对变化的准备。
同样的事情也发生在软件方面。软件更新迭代很快,会经常需要重构,需求也会频繁的变更,这完全在我们的预期之外。
所以,不要把自己当做软件工程师,要把自己当做城市规划者。让你的软件有机成长,按需适应。兵来将挡,水来土掩,问题来了就去解决,但是要确保一切都有其所在。
在软件领域做这些事情比在城市里做这些事情要容易的多,因为软件是灵活的,土木工程并不是,在软件世界里,我们的“建筑时长”就是编译时长。在巴塞罗那我们不能通过简单地毁掉建筑给新的建筑腾出空地儿,但是在软件世界里我们可以非常简单的实现。我们随时可以推翻重做,随时可以做实验,我们想构建几次就构建几次,时间花费也就那么几秒钟,但是我们思考的时间却比构建的要多得多。我们的工作是纯智力型的。
所以把自己当做城市规划者,让你的软件按需成长、适应。
通过这样做,你就能够更好的抽象,也会知道在什么合适的时间来采用它们。
正如 Sam Koblenski 所说:
抽象只适用于合适的语境,而合适的语境是随着系统的发展而发展的。
有时候我经常会看到这种现象:有些同学在学习一项新技术时会去寻找模板,但是在我看来,当你开始学习的时候,应当避免使用模板。当然,如果你已经有了一定经验,那么模板和生成器还是很有用的。模板剥夺了大部分的控制权,因此你就学不到如何去新建一个工程,你也无法准确理解每个代码片段适合哪里。
当你无法轻轻松松地把事情搞定,当你感受到的都是苦苦的挣扎时,也许是时候另辟蹊径了。我们的宗旨是力争懒惰,你应该为了以后不工作的目标而去工作。如此一来,你就会有更多的自由时间去做其他的事情,从而减低了成本,提升了收益,所以这也是你达成目的的另一条途径。你不应该没头没脑的努力工作,你要更加聪明的工作。
也许有些人也会拥有你现在的烦恼,但是如果没人这样做的话,那么你的机会就到了,你可以找到你自己的解决方法,可以给其他人伸出援助之手。
但是有时候在你没有看到别人做的更优秀之前,你并不会意识到其实你也可以做的更高效。这就是与人交流的重要性。
通过和其他人交流,你可以分享你的经验,为对方的职业生涯提供帮助,也能发现能够提升我们工作流的新工具,而且更重要的是,你能够学到解决问题的方法。这就是我喜欢阅读分享公司解决问题方法的文章的原因。
尤其是在我们的领域里,我们总是认为 Google 和 StackOverflow 能够回答我们的所有问题,但是我们仍然有必要知道我们要问的问题。我确定会有这么一种场景:你遇到了一个你无法解决的问题,因为你无法准确的知道发生了什么事情,所以你自己都不清楚你应该问什么问题。
但是如果我需要用一个建议来总结整篇文章的话,那就是:
解决问题。
软件不是魔法箱,也不是一首诗(很不幸)。它的存在是为了解决问题,提高人们的生活水平。软件的存在是为了让世界向前发展。
年轻人,该你出动解决问题了。