不要使用这些编程语言特性及模式

不可避免的语言特性不会在文章里提到。

语法层面的getter和setter支持

绝大多数现代编程语言都支持用点运算符(.)访问对象的属性,有一部分编程语言还支持覆盖掉语言默认的setter和getter,后面这种支持是有害的。

用点运算符访问属性是如此常见,这使得它本身就带有一种约定俗成的暗示:属性的读取和写入是非常快的内存访问,代码的执行成本应该无限接近于零。自定义的getter和setter会破坏这种暗示,对象的使用者可能因此意外引入高开销的操作。

这就是为什么仍然应该手动用setXXXgetXXX方法定义这些自定义行为,因为这可以让人直观地意识到它们可能带来潜在的性能影响:

  • 用点运算符的属性访问总是安全、高效、直白的,开发人员总是可以放心使用它们而不用担心性能问题。

  • 当遇到setXXXgetXXX这样的方法时,开发人员会意识到这些操作可能带来潜在的性能影响,从而谨慎地使用它们。

Pointfree风格——柯里化和偏函数

柯里化和偏函数是函数式编程里的糟粕,这两项特性会让开发人员轻而易举地创造出很多临时函数,基于这两个糟粕发展出的Pointfree风格更是糟粕中的糟粕。

在语言层面上,很多编程语言对临时函数的类型签名都支持得很粗糙,比Haskell那个只有参数类型的倒霉的类型签名好不到哪去。

在工程层面上,给这些临时函数命名是一个大问题,很多时候开发人员根本找不到一个合适的名字,于是函数的功能会慢慢发展成一个谜团。即使对那些创造出这些临时函数的人来说,也会很快忘记它们到底是干什么用的。理解这些临时函数需要读者向上回溯每一层临时函数,最终把阅读代码变成一项考古研究。

具名匿名函数

创建一个匿名函数,然后把它赋值给一个变量……为什么不直接创建一个函数?

具名匿名函数是这个世界上最没有用的东西之一,尤其在那些支持嵌套函数和函数声明提升的编程语言里:

// 具名匿名函数
function main(...args) {
// 具名匿名函数需要按顺序创建在代码块头部, 因为后面的代码会使用它们
const getValue = () => {
// ...
}
// 开发人员需要先向下翻找到程序的主要逻辑
// 主要逻辑堆积在代码块的尾部, 离函数参数/文件头部的垂直距离太远
doSomething(getValue())
}
// 直接创建函数
function main(...args) {
// 主要逻辑在代码块的头部, 离函数参数/文件头部的垂直距离更近
doSomething(getValue())
// 函数声明提升使程序员无需关注函数的声明顺序
// 合理的函数命名让开发人员在阅读代码时不需要关注函数的实施细节,
// 大多数函数都不需要被阅读, 因此将它们放在尾部很自然
function getValue() {
// ...
}
}

运算符重载

所以, 在这里用++=可以为委托添加一个监听器……天哪。

且不说二元运算符对一些操作来说根本不够用的问题,一些语言设计者和程序员恐怕不明白为什么应该正确地给事物命名,把操作命名成一个与它含义相近的“别的东西”只是在为日后的混乱埋下伏笔。

运算符重载在一些编程语言和库里被滥用了,除了有明确数学定义以外的类型都不应该使用运算符重载。

// 坏的语言设计
delegate += listener // 似乎很酷, 但你怎么添加一个一次性监听器?
// 好的语言设计
emitter.addListener(listener) // 添加监听器
emitter.addListener(listener, { once: true }) // 添加一次性监听器

支持自动分号插入(ASI)的语言里的分号

帮自己的小指一个忙,不要在支持自动分号插入的语言里使用分号。

在这些语言里不写分号的理由很简单:如果你不写分号,在99%的情况下代码不会出错。在你不小心忘记写分号的时候,代码有极高概率仍然能正常运行,除非linter提醒,否则极大概率你根本不会发现自己漏写了分号。既然如此,为什么要写分号?你只要记得需要使用分号的情况就好了。

分号捍卫者的一种常见辩解是记忆自动插入规则的例外很麻烦,会增加程序员的负担——这完全是放屁,事实上绝大多数会用到例外的模式都是你作为程序员不应该使用的模式。

我随便就可以举出两个JavaScript里的分号如何鼓励不良的编程模式的例子,这两个例子可以回击几乎所有所谓“不应该省略分号”的说法。

JavaScript例子1: 数组

// 使用分号
function main() {
const map = {/* ... */};
[1, 2, 3].forEach(x => {
// ...
});
}
// 去掉分号之后出错, 分号捍卫者会说这就是为什么不应该省略分号
function main() {
const map = {/* ... */}
// TypeError: Cannot read properties of undefined (reading 'forEach')
['for', 'while', 'if', 'else'].forEach(x => {
// ...
})
}
// 然而, 比起去掉分号会出错, 上述代码更大的问题是数组没有命名
// 遵循好的代码编写习惯, 无分号根本不会造成问题, 你甚至不需要记忆例外, 因为你根本碰不到例外
function main() {
const map = {/* ... */}
const keywords = ['for', 'while', 'if', 'else']
keywords.forEach(x => {
// ...
})
}

JavaScript例子2: IIFE

// 使用分号
function main() {
const list = [/* ... */];
(() => {
// ...
})();
}
// 去掉分号出错, 分号捍卫者说这就是为什么不应该省略分号
function main() {
const list = [/* ... */]
// TypeError: [] is not a function
(() => {
// ...
})()
}
// 遵循好的代码编写习惯, 无分号根本不会造成问题, 你甚至不需要记忆例外, 因为你根本碰不到例外
import { go } from '@blackglory/go' // go函数实际上只是`fn => fn()`
function main() {
const list = [/* ... */]
// 分号捍卫者会说额外编写一个go函数是小题大做, 我会说这些人代码写得太少, 以至于从来没漏写过IIFE最后的`()`
go(() => {
// ...
})
}

隐式类型转换

隐式类型转换是黑魔法,远离黑魔法。

动态添加/删除/修改实例成员的类型或行为

动态语言最大的糟粕就是可以轻而易举地在运行时修改一个类的实例的成员的类型或行为。这是我在代码审查过程中最不想见到的编程模式,因为相关代码非常难以追踪和理解,复杂程度比在多线程代码里共享变量还要高得多。

重量级函数式编程

函数式编程可以分为两种,轻量级的和重量级的。

区分两者很简单,重量级的范式会引入大量晦涩难懂的原语,需要你搞懂什么是Monad,什么是Functor——通常来说,看到这两个词的时候就应该像看到有毒物质警示标志一样引起警惕。

远离重量级函数式编程范式的原因很简单:

  • 99%的程序员都不是搞数学出身的,即使你是,别人也读不懂你的代码。

  • 重量级函数式编程在工程上不是那么有用,所以没必要使用——既然没必要,就别用。