这篇文章作者David Herron过去是Java鼓吹手,现在感觉自己明白过来了,推崇JavaScript了。以下原文大意:
在Sun公司的JavaSE团队工作了10多年的人,还在为了用Java字节码实例化一个抽象接口一直拼命到最后一口气。对于我这位前JavaSE团队成员来说,2011年学习Node.js平台好比接触到一种新鲜空气。在2009年1月被Sun解雇后(就在甲骨文收购之前),我了解了Node.js并迷上了它。
有多着迷?自2010年以来,我撰写了大量关于Node.js编程的文章。四版Node.js Web开发,还有其他书籍和大量关于Node.js编程的教程博客文章。
当我在Sun公司工作时,我相信所有的东西都是Java的。我在JavaONE上参与了一些会议,共同开发了java.awt.Robot类,运行了Mustang回归竞赛(Java1.6版本的bug发现竞赛),帮助启动了“Java发行许可证“,这是用于分发JDK构建的Linux发行版,也就是OpenJDK之前的方案,后来也在OpenJDK项目中扮演了一个小角色。在此过程中,我在java.net(一个现已失效的网站)上发布博客,每周写1-2次,讨论Java生态系统中的事件,为期6年,一个重要的话题是保护Java不受那些预测Java死亡的人的影响。
杜克奖颁发给了那些超越一切的员工。我是在运行Mustang回归竞赛(bug发现竞赛)和Java1.6版本的同时获得这一成绩的。
我在这里的目的是解释为什么我这个Java死对头会变成这Node.js/JavaScript拥护者的。
我并没有完全脱离Java,在过去3年中,我编写了大量的Java/Spring/Hibernate代码。我非常喜欢我的工作,我在太阳能行业工作,做了一些令人心旷神怡的事情,比如用JAVA编写有关千瓦时 - 编码的数据库查询,但Java已经失去了光彩。
两年的Spring编码给出了一个非常清楚的教训:对复杂性进行样板化模式化框架化并不能产生简单性,它只会产生更多的复杂性。
1. Java充满了样板代码,这反而干扰掩盖了程序员编码的意图。为了使用模板化代码,调整或放弃自己原来的设计意图,因为必须按照它的路子才能耍。
2.Spring&Spring Boot已经给我们上了一课:把复杂性进行框架化掩盖起来只会产生更多的复杂性。
3.JavaEE是一个“按委员会领导意图设计”的项目,涵盖了企业应用程序开发中的一切,因此极其复杂。
4.Spring编程的经验是,它一直是很伟大直到它不再伟大。直到有那么一天,一个模糊的,不可能理解的异常出现在一个深度子系统,你从未听说过需要3天以上的时间来解决这个问题。
5.框架中为了让编码器编写零代码所付出的代价你们想过吗?是多少?
6.虽然IDE Eclipse功能强大,但它们是Java复杂性的一个症状,因为复杂语言催生了复杂的开发工具。
7.Node.js是一个人经过磨练和完善,希望使用轻量级事件驱动架构的结果,当Node.js出现以后,JavaScript社区似乎很喜欢删除样板,这样程序员才可以发光了,摆脱教条主义形式主义的约束了。
8.回调Hell可以使用异步/等待函数解决,这是一个删除样板模式的例子,这样程序员的意图就会显现出来。否则程序员用回调模式只能写出地狱般的代码。
9.用Node.js编写代码是一种乐趣
10.JavaScript缺乏Java的严格类型检查,这也是一种祝福或危险。好处是代码更容易编写,但需要更多的测试以确保正确性。
11.NPM/yarn包装管理系统是优秀的,使用是快乐的,这是相对于令人厌恶的是Maven而言。
12.Java和Node.js都提供了出色的性能,这与JavaScript速度慢的神话其实背道而驰,谎言说Node.js的性能肯定很差,谷歌为了加快Chrome浏览器的运行速度,帮助了Node.js的性能,因为谷歌加大了在V8上的投资。浏览器之间的激烈竞争使得JavaScript变得越来越强大,Node.js从中受益。
Java已经成为一种负担,用Node.js编写代码是充满喜悦的
一些工具或对象是设计师花了多年时间磨练和完善的结果。他们尝试了不同的想法,删除了不必要的属性,最终得到了一个具有正确属性的对象。这些对象通常具有一种强大的简单性,非常吸引人。Java不是那种系统。
Spring是基于Java开发Web应用程序的流行框架。Spring的核心用途,特别是SpringBoot,是一个易于使用的预配置JavaEE堆栈。Spring程序员不需要连接所有的servlet、数据持久性、应用服务器等,谁知道还有什么,这样才能得到一个完整的系统。相反,Spring负责所有这些细节,而你则专注于编码。例如,JPARepository类合成数据库查询方法的名称,如“findUserByFirstName” - -你不编写任何代码,只需将以这种方式命名的方法添加到Repository定义中,Spring将处理其余的方法。
这是一个伟大的故事,一个美好的经历,但是最后你会发现它不是。
当你得到Hibernate PersistentObjectException错误时,关于传递到持久化的独立实体错误?这需要几天时间才能找出原因,这可能是过于简化导致 ,这意味着到达REST端点的JSON必须是带有值的ID字段。Hibernate过于简单,但需要控制ID值,否则抛出这个令人困惑的异常。有数千条同样令人困惑和迟钝的异常消息。对于Spring堆栈中的一个子系统又一个子系统,它就像一个死对头坐在那里等待你犯最微小的错误,然后它用一个应用程序崩溃的异常向你猛扑过来。
然后是巨大的堆栈日志记录,他们可持续显示几个屏幕,里面充满了抽象的方法。Spring显然正在制定实现代码所需的配置。这个抽象级别显然需要相当多的逻辑来查找所有内容并执行请求。很长的堆栈跟踪并不一定是坏的。相反,它指出了一个症状:内存/性能开销是多少?
当程序员编写零代码时,“findUserByFirstName”将如何执行?该框架必须解析方法名,猜测程序员的意图,构造类似抽象语法树的内容,生成一些SQL等等。这一切的开销是多少?这样编码器就不用编码了?
在经历了几十次之后,花了几个星期的时间去学习你不应该学到的奥秘,你可能会得出和我一样的结论:把复杂写在纸上隐藏起来并不能产生简单,它只会产生更多的复杂性。
重点是Node.js
“兼容性问题”是一个非常酷的口号,意味着Java平台的关键价值主张是完全向后兼容。我们把这件事太当回事了,把它涂在像自己的T恤上。当然,这种程度的兼容性维护可能是个累赘,有时逃避那些不再有用的老方法反而是有用的。
另一方面,Node.js则是…。
在Spring和JavaEE极其复杂的地方,Node.js是新鲜空气。首先是用于开发核心Node.js平台的设计美学经验Ryan Dahl,为一个重量级的复杂系统使用线程,他寻找了一些不同的东西,花了几年时间磨练和完善了一套核心理想,成就了Node.js。结果是一个轻量级的系统、一个单一的执行线程、巧妙地使用JavaScript匿名函数进行异步回调,以及一个巧妙实现异步性的运行库。可以使用事件传递到回调函数实现高吞吐量的事件处理。
还有JavaScript语言本身。JavaScript程序员似乎有一种删除样板的审美观,这样程序员的意图就能清晰地显现出来。
Java和JavaScript之间对比的一个例子是监听器函数的实现。在java,侦听器需要创建抽象接口类的具体实例,这需要大量的言语来掩盖正在发生的事情。程序员的意图怎么能在样板的面纱后面被看到呢?
在JavaScript中,可以使用简单的匿名函数 - 闭包,你不需要搜索正确的抽象接口,相反,你只需编写所需的代码,而不需要过多的语句。
另一种学习:大多数编程语言模糊了程序员的意图,使理解代码变得更加困难。
这一点也涉及到Node.js - ,但是我们必须指出一个警告:回调地狱,解决办法有时伴随着带入他们自己的问题。
在JavaScript中,异步编码长期以来一直存在两个问题,一个是Node.js中所谓的“回调地狱”;很容易陷入深度嵌套回调函数的陷阱,在这种情况下,每一层嵌套都会使代码变得复杂,从而使错误和结果处理变得更加困难。
一个相关的问题是JavaScript语言没有帮助程序员正确地表达异步执行,出现了几个库,它们承诺简化异步执行,这是另一个用纸张掩盖复杂性的例子,创造了更多的复杂性。
举一个例子:
const async = require(‘async’);
const fs = require(‘fs’);
const cat = function(filez, fini) {
async.eachSeries(filez, function(filenm, next) {
fs.readFile(filenm, ‘utf8’, function(err, data) {
if (err) return next(err);
process.stdout.write(data, ‘utf8’, function(err) {
if (err) next(err);
else next();
});
});
},
function(err) {
if (err) fini(err);
else fini();
});
};
cat(process.argv.slice(2), function(err) {
if (err) console.error(err.stack);
});
这个示例应用程序是对unix的cat命令的简单模仿 。异步库async在简化异步执行的排序方面非常出色,但是它的使用需要一堆样板代码来掩盖程序员的意图。
我们这里需要有一个循环,但是没有作为循环编写的,也不使用自然循环结构。此外,错误和结果不方便地被困在回调函数中。在ES 2015/2016功能加入Node.js之前,这是我们所能做的最好的。
Node.js 10.x版本等效做法是:
const fs = require(‘fs’).promises;
async function cat(filenmz) {
for (var filenm of filenmz) {
let data = await fs.readFile(filenm, ‘utf8’);
await new Promise((resolve, reject) => {
process.stdout.write(data, ‘utf8’, (err) => {
if (err) reject(err);
else resolve();
});
});
}
}
cat(process.argv.slice(2)).catch(err => {
console.error(err.stack);
});
使用异步/等待(asnc/await)功能对前面示例进行了重写。他们有相同的异步结构,但是使用普通循环结构编写的。错误和结果是以自然的方式展现。它更容易阅读,代码,理解,主动表达了程序员的意图。
唯一的毛病是process.stdout.写不提供承诺接口,因此如果不使用Promise,就不能在异步函数中干净地使用。
调用地狱的问题并没有通过掩盖复杂性来解决,相反,语言和范式的改变既解决了问题,也解决了临时解决方案强加给我们的过度言辞,带着异步我们的代码变得更漂亮了。
这是优秀的解决方案带来Node.js和JavaScript的闪光点。
假定通过定义良好的类型和接口来实现清晰
我作为一个死气沉沉的Java倡导者曾经强调的一句教条是:严格的类型检查可以编写巨大的应用程序。当时的规范是开发单一系统(没有微服务,没有Dcoker等等)。因为Java有严格的类型检查,所以Java编译器通过防止编译糟糕的代码来帮助你避免许多类型的错误( - )。
相比之下,JavaScript的打字方式很松散。
理论是显而易见的:程序员无法确定他们收到了什么样的对象,那么程序员如何才能知道该做什么呢?
Java中在严格输入的另一面却是需要更多的样板。程序员不断地进行打字或努力工作,以确保一切都是正确的。编码器花费时间使用更多的样板以极高的精度编译,希望用更少时间能捕捉到和纠正早期的错误。
这个问题是如此之大,以致于必使用大型复杂IDE,一个简单的程序员编辑器是不够的。让Java程序员保持理智(除了披萨)的唯一方法是下拉显示对象上的可用字段,描述方法参数,帮助构造类,协助重构,以及Eclipse、NetBeans和IntelliJ提供的所有其他工具。
别让我对Maven动手动脚。真是个可怕的工具。
在JavaScript中,变量类型不会被声明,类型转换通常不会被使用,等等。因此,代码读起来更清晰,但也存在未明编码错误的风险。
这一点是Java好处还是坏处?取决于你的观点。十年前,我的看法是,通过获得更多的确定性,这种间接费用是值得的;我今天的观点是天哪,这需要花费很多工作,而且用JavaScript的方式做事情要容易得多。
用容易测试的小模块清除bug
js鼓励程序员将程序划分为小单元,即模块。这似乎是一件小事,但它在一定程度上解决了刚刚提到的问题。
一个模块是:
1. 自我包含- 顾名思义,它将相关代码打包到一个单元中
2. 强边界-模块内的 代码是安全的,不受其他代码的入侵
3. 显性出口-默认情况下不导出模块中的 代码和数据,所选函数和数据才可供其他代码使用。
4. 显式进口- 模块声明它们所依赖的模块
5. 潜在独立- 很容易将模块公开发布到npm存储库,或者在其他地方私下发布模块,以便在应用程序之间方便地共享
6. 更容易推理-减少需要阅读的 代码,使人们更容易理解目的
7. 易于测试- 如果实现正确,小模块可以很容易地进行单元测试
所有这些都有助于Node.js模块更容易测试并具有定义良好的范围。
担心JavaScript的心理在于,如果缺少严格的类型检查,代码很容易出错。但是,在一个边界清晰的小聚焦模块中,受影响代码的范围主要限于该模块。这使得大多数关注保持小而安全地安置在该模块的边界内。
解决松耦合问题的另一个解决方案是增加测试。
你必须将一些效率提高(编写JavaScript代码更容易)才能有时间用于增加测试。你的测试机制必须捕获编译器可能捕获的错误。你要自己测试你的代码,不是吗?
对于那些想要在JavaScript中实现静态检查类型的人,请看TypeScript。我没有用过那种语言,但听过一些很棒的话。它与JavaScript直接兼容,并添加了有用的类型检查和其他特性。
这里的重点是Node.js和JavaScript。
包装管理
我一想到Maven就会中风,只是脑子不清醒,不能写出任何关于Maven的东西。据推测,一个人要么爱Maven,要么鄙视它,而且没有中间立场。
一个问题是Java生态系统没有一个具有凝聚力的包管理系统。Maven包存在并运行得相当好,而且据说也在Gradle中工作。但是它并不像Node.js的包管理系统那样有用/可用/强大。
在Node.js世界中,有两个优秀的包管理系统紧密地协同工作。起初,NPM和NPM存储库是唯一这样的工具。
有了NPM,我们就有了一个很好的描述包依赖关系的模式。依赖项可以是严格的(确切地说是1.2.3版本),也可以是通过几个松散级别指定的,直到“*”,这意味着最新版本。.js社区已经将成千上万的包发布到NPM存储库中。使用来自NPM存储库之外的包与使用NPM存储库中的包一样容易。
NPM的存储库不仅为Node.js服务,而且还为前端工程师服务。以前使用过包管理工具,比如Bower。Bower已经被废弃了,现在人们发现了所有前端JavaScript库都可以作为NPM包使用。许多前端工程师工具链,如Vue.js、CLI和Webpack,都是用Node.js编写的。
Node.js的另一个包管理系统SEAR是从NPM存储库中提取其包,并使用与NPM相同的配置文件。主要的优点是yarn工具跑得更快。
NPM存储库,无论是用NPM访问还是用yarn访问,都是使Node.js如此容易和快乐地使用的强大部分。
性能
有时,Java和JavaScript都被指责速度慢。
这两种语言共同点:编译器将源代码转换为由虚拟机实现执行的字节代码。VM通常会进一步将字节码编译成本机代码,并使用各种优化技术。
Java和JavaScript都有快速运行的巨大潜力。在Java和Node.js中,激励机制是更快速服务器端代码,在Browser-JavaScript中,奖励是更好的客户端应用程序性能, - 参见关于RichInternet应用程序的下一节。
Sun/OracleJDK使用HotSpot,这是一个具有多字节代码编译策略的超级DOOPER虚拟机。它的名称来自于检测频繁执行的代码,并在代码节执行越多时应用越来越多的优化。HotSpot是高度优化的,并产生非常快的代码。
在JavaScript方面,我们过去常常想:我们怎么能期望运行在浏览器中的JavaScript代码实现任何类型的复杂应用程序?当然,在基于浏览器的JavaScript中,办公文档处理套件是不可能的?今天,我正在用GoogleDocs写这篇文章,而且性能很好。浏览器-JavaScript性能每年都有飞跃。
由于使用Chrome的V8引擎,Node.js直接受益于这一趋势。
一个例子是PeterMarshall的演讲,他是一位从事V8开发的Google工程师,他的主要工作是提高V8的性能。Marshall专门负责Node.js性能方面的工作。他描述了为什么V8从曲轴虚拟机切换到Turbofan虚拟机。
机器学习是一个涉及很多数学的领域,数据科学家通常使用R或Python。包括机器学习在内的几个领域都依赖于快速数值计算。由于各种原因,JavaScript在这方面做得很差,但是开发一个标准化的JavaScript数值计算库的工作正在进行中。
在另一次演讲中,IBM的ChrisBailey讨论了Node.js的性能和可伸缩性问题,特别是在Docker/Kubernetes部署方面。他从一组基准测试开始,显示Node.js在I/O吞吐量、应用程序启动时间和内存占用方面的性能明显优于SpringBoot。此外,Node.js的逐版本性能正在显著提高,这在一定程度上要归功于V8的改进。
在视频中,Bailey说人们不应该在Node.js中运行计算代码。其中的“为什么”很重要,这是因为单线程模型,长时间运行的计算会阻止事件的执行。
如果说这些JavaScript改进对应用程序来说还不够,那么有两种方法可以直接将原生代码集成到Node.js中。最简单的方法是使用一个node-gyp的Node.js模块。可以处理到原生本地代码模块的链接。
WebAssembly提供了将其他语言编译成运行非常快的JavaScript子集的能力。WebAssembly是在JavaScript引擎中运行的可执行代码的可移植格式。
丰富的因特网应用程序(RIA)
十年前,软件业在用加速JavaScript引擎来实现富互联网应用程序,这些引擎会使桌面应用程序变得不再重要。
这个故事实际上始于二十多年前,Sun和Netscape就Netscape Navigator中的Java小程序达成了一项协议,JavaScript语言部分是作为Javaapplet的脚本语言开发的,希望服务器端有JavaServlet,客户端有Javaapplet,这给了我们在这两种语言上都有相同编程语言的条件。这种情况因为各种原因而并没有发生。
十年前,JavaScript开始变得足够强大,可以单独实现复杂的应用程序,因此,RIA的流行词,RIA应该是杀死了Java作为一个客户端应用程序平台的可能。
今天,我们开始看到RIA的想法实现了,使用服务器上的Node.js,我们现在可以使用Nirvana,但是在连接的两端使用JavaScript。
一些例子:
1. GoogleDocs(本文正在编写的地方)就像一个典型的Office套件,但在浏览器中运行
2. 强大的框架,如Reaction、Rangle、Vue.js,简化了使用HTML/css进行样式化的基于浏览器的应用程序开发。
3. Electron是Node.js和Chromium Web浏览器的混合体,支持跨平台桌面应用程序开发。一些非常流行的应用程序,如VisualStudioCode、Atom、GitKraken和Postman都是用它编写的,它们的性能都非常好。
4. 因为Electron/NW.js使用的是浏览器引擎,所以Reaction/Angular/Vue这样的框架可以用于桌面应用程序。
Java作为桌面应用程序平台并没有因为JavaScriptRIA而消亡,它的死亡主要是由于SUN公司对客户端技术的忽视,Sun专注于要求快速服务器端性能的企业客户,我当时在场,亲眼看到了。
真正扼杀applet的是几年前在Java插件和JavaWebStart中发现的一个糟糕的安全漏洞,该bug导致全球范围内的警报停止使用Javaapplet和Webstart应用程序。
还可以开发其他类型的Java桌面应用程序,因此NetBeans和EclipseIDE之间的竞争依然存在,但是在Java这一领域的工作是停滞不前的,在开发工具之外,很少有基于Java的应用程序。
JavaFX是例外。
10年前,JavaFX将成为Sun对iPhone的回应。它将支持在手机中可用的Java平台上开发图形用户界面丰富的应用程序,并试图单枪匹马地将Flash和IOS应用程序开发排除在外,但这没有变成现实,JavaFX仍在使用,但没有达到它的炒作效果。
这个领域的所有兴奋都发生在Reaction、Vue.js和类似的框架中。
在本例中,JavaScript和Node.js在很大程度上赢得了这一点。
结束
今天,开发服务器端代码有许多选择。我们不再局限于“P语言”(Perl、PHP、Python)和Java,因为还有Node.js、Ruby、Haskell、Go、Rust等等。因此,我们有一种尴尬的财富可以享受。
关于为什么Java会转向Node.js,很明显,我更喜欢用Node.js编写代码时的自由感。Java成了负担,对于Node.js来说,没有这样的负担。如果我再次受雇于编写Java,我当然会用Java编写,因为我被付了工资的。
每个应用程序都有其真实的需求,当然,不能因为人们更喜欢Node.js就总是使用Node.js,这也是不正确的。必须有技术上的理由来选择一种语言或框架而不选择另一种语言或框架。