您正在阅读我关于干净代码的书“清洗代码”的摘录。提供 PDF、EPUB、平装本和 Kindle 版本。立即获取副本。
聪明的代码是我们在工作面试问题或语言测验中可能会看到的东西,当他们希望我们知道我们以前可能从未见过的语言功能是如何工作的时。我对所有这些问题的回答是:“它不会通过代码审查”。
有些人将简洁与清晰度混淆。短代码(简洁)并不总是最清晰的代码(清晰),通常情况恰恰相反。努力让代码更短是一个崇高的目标,但它不应该以牺牲可读性为代价。
在代码中表达相同想法的方法有很多种,有些比其他更容易理解。我们应该始终致力于减少下一个阅读我们代码的开发人员的认知负担。每当我们偶然发现一些不太明显的东西时,我们就浪费了我们的大脑资源。
信息:我从 Steve Krug 的同名网络可用性书中“窃取”了本章的名称。
让我们看一些例子。尝试涵盖答案并猜测这些代码片段的作用。然后,数数你猜对了多少个。
示例1:
const percent = 5; const percentString = percent.toString().concat('%');
此代码仅将 % 符号添加到数字中,应重写为:
const percent = 5; const percentString = `${percent}%`; // → '5%'
示例2:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
~ 符号称为 按位 NOT 运算符。它在这里的有用作用是,仅当 indexOf() 返回 -1 时,它才返回一个假值。这段代码应该重写为:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
示例 3:
const value = ~~3.14;
按位 NOT 运算符的另一个晦涩用法是丢弃数字的小数部分。使用 Math.floor() 代替:
const value = Math.floor(3.14); // → 3
示例 4:
if (dogs.length + cats.length > 0) { // Something fishy here… }
这个很快就可以理解了:它检查两个数组中的任何一个是否有任何元素。不过,最好说清楚一点:
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
示例 5:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
这个问题我花了一段时间才明白。假设我们有 URL 的一部分,例如 filename="pizza"。首先,我们用 = 分割字符串并获取第二部分“pizza”。然后,我们将第一个和最后一个字符切片以获得披萨。
我可能会在这里使用正则表达式:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
或者更好的是 URLSearchParams API:
const header = 'filename="pizza.rar"'; const filename = new URLSearchParams(header) .get('filename') .replaceAll(/^"|"$/g, ''); // → 'pizza'
不过这些引用很奇怪。通常我们不需要在 URL 参数周围加上引号,因此与后端开发人员交谈可能是个好主意。
示例 6:
const percent = 5; const percentString = percent.toString().concat('%');
在上面的代码中,当条件为真时,我们向对象添加一个属性,否则我们什么都不做。当我们显式定义要解构的对象而不是依赖于虚假值的解构时,意图更加明显:
const percent = 5; const percentString = `${percent}%`; // → '5%'
我通常更喜欢对象不改变形状,所以我会移动值字段内的条件:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
示例7:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
这个奇妙的单行代码创建了一个充满从 0 到 9 的数字的数组。Array(10) 创建了一个包含 10 个 空 元素的数组,然后 keys() 方法返回键(从 0 开始的数字)到 9) 作为迭代器,然后我们使用扩展语法将其转换为普通数组。爆炸头表情符号…
我们可以使用 for 循环重写它:
const value = ~~3.14;
尽管我喜欢避免代码中出现循环,但循环版本对我来说更具可读性。
中间的某个地方将使用 Array.from() 方法:
const value = Math.floor(3.14); // → 3
Array.from({length: 10}) 创建一个包含 10 个 未定义 元素的数组,然后使用 map() 方法,我们用 0 到 9 的数字填充该数组。
我们可以使用 Array.from() 的映射回调将其写得更短:
if (dogs.length + cats.length > 0) { // Something fishy here… }
显式的map()可读性稍好一些,我们不需要记住Array.from()的第二个参数的作用。此外,Array.from({length: 10}) 比 Array(10) 更具可读性。虽然只有一点点。
那么,你的分数是多少?我想我的应该是3/7左右。
一些模式介于聪明和可读性之间。
例如,使用布尔值过滤掉虚假的数组元素(本例中为 null 和 0):
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
我觉得这种模式可以接受;虽然它需要学习,但它比其他选择要好:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
但是,请记住,这两种变体都会过滤掉 falsy 值,因此如果零或空字符串很重要,我们需要显式过滤未定义或 null:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
当我看到两行看起来相同的棘手代码时,我认为它们在某些方面有所不同,但我还没有看到差异。否则,程序员可能会为重复的代码创建一个变量或函数,而不是复制粘贴它。
例如,我们有一段代码可以为我们在项目中使用的两个不同工具(Enzyme 和 Codeception)生成测试 ID:
const header = 'filename="pizza.rar"'; const filename = new URLSearchParams(header) .get('filename') .replaceAll(/^"|"$/g, ''); // → 'pizza'
很难立即发现这两行代码之间的差异。还记得那些需要找出十个不同点的图片吗?这就是这段代码对读者的作用。
虽然我通常对极端的代码干燥持怀疑态度,但这是一个很好的例子。
信息:我们在分而治之或合并与放松章节中详细讨论了“不要重复自己”原则。
const percent = 5; const percentString = percent.toString().concat('%');
现在,毫无疑问,两个测试 ID 的代码是完全相同的。
让我们看一个更棘手的例子。假设我们对每个测试工具使用不同的命名约定:
const percent = 5; const percentString = `${percent}%`; // → '5%'
这两行代码之间的差异很难注意到,而且我们永远无法确定名称分隔符(- 或 _)是这里唯一的差异。
在有这样需求的项目中,这种模式很可能会出现在很多地方。改进它的一种方法是创建为每个工具生成测试 ID 的函数:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
这已经好多了,但还不够完美——重复的代码仍然太大。让我们也解决这个问题:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
这是使用小函数的极端情况,我通常会尽量避免如此拆分代码。然而,在这种情况下,它效果很好,特别是如果项目中已经有很多地方我们可以使用新的 getTestIdProps() 函数。
有时,看起来几乎相同的代码存在细微的差异:
const value = ~~3.14;
这里唯一的区别是我们传递给函数的参数具有很长的名称。我们可以在函数调用中移动条件:
const value = Math.floor(3.14); // → 3
这消除了类似的代码,使整个代码片段更短且更易于理解。
每当我们遇到使代码略有不同的条件时,我们应该问自己:这个条件真的有必要吗?如果答案是“是”,我们应该再问自己一次。通常,对于特定条件并没有真正需求。例如,为什么我们甚至需要为不同的工具分别添加测试ID?我们不能配置其中一个工具来使用另一个工具的测试 ID 吗?如果我们挖掘得足够深,我们可能会发现没有人知道答案,或者最初的原因不再相关。
考虑这个例子:
if (dogs.length + cats.length > 0) { // Something fishy here… }
此代码处理两种边缘情况:当 assetDir 不存在时,以及当 assetDir 不是数组时。此外,对象生成代码是重复的。 (我们不要谈论嵌套三元组......)我们可以摆脱重复和至少一个条件:
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
我不喜欢 Lodash 的castArray() 方法将 undefined 包装在数组中,这不是我所期望的,但结果仍然更简单。
CSS 具有简写属性,开发人员经常过度使用它们。这个想法是单个属性可以同时定义多个属性。这是一个很好的例子:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
等同于:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
一行代码而不是四行,仍然很清楚发生了什么:我们在元素的所有四个边上设置了相同的边距。
现在,看这个例子:
const percent = 5; const percentString = percent.toString().concat('%');
要了解他们的作用,我们需要知道:
这会造成不必要的认知负担,并使代码更难以阅读、编辑和审查。我避免使用这种简写。
速记属性的另一个问题是它们可以为我们不打算更改的属性设置值。考虑这个例子:
const percent = 5; const percentString = `${percent}%`; // → '5%'
该声明设置了 Helvetica 字体系列,字体大小为 2rem,并使文本变为斜体和粗体。我们在这里没有看到的是,它还将行高更改为默认值正常。
我的经验法则是仅在设置单个值时才使用简写属性;否则,我更喜欢手写属性。
这里有一些很好的例子:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
以下是一些需要避免的示例:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
虽然简写属性确实使代码更短,但它们通常会使其难以阅读,因此请谨慎使用它们。
消除条件并不总是可能的。但是,有一些方法可以使代码分支中的差异更容易被发现。我最喜欢的方法之一就是我所说的并行编码。
考虑这个例子:
const value = ~~3.14;
这可能是我个人的烦恼,但我不喜欢返回语句处于不同的级别,这使得它们更难以比较。让我们添加一个 else 语句来解决这个问题:
const value = Math.floor(3.14); // → 3
现在,两个返回值都处于相同的缩进级别,使它们更容易比较。当没有条件分支处理错误时,此模式有效,在这种情况下,尽早返回将是更好的方法。
信息:我们在避免条件章节中讨论提前退货。
这是另一个例子:
if (dogs.length + cats.length > 0) { // Something fishy here… }
在此示例中,我们有一个按钮,其行为类似于浏览器中的链接,并在应用程序中显示确认模式。 onPress 属性的相反条件使这个逻辑很难看出。
让两个条件都为正:
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
现在,很明显我们可以根据平台设置 onPress 或 link 属性。
我们可以停在这里或更进一步,具体取决于组件中 Platform.OS === 'web' 条件的数量或我们需要有条件设置的 props
我们可以将条件道具提取到一个单独的变量中:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
然后,使用它而不是每次都硬编码整个条件:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
我还将 target 属性移至 Web 分支,因为应用程序无论如何也不会使用它。
当我二十多岁的时候,记住事情对我来说并不是什么问题。我可以回忆起我读过的书以及我正在从事的项目中的所有功能。现在我四十多岁了,情况不再是这样了。我现在看重不使用任何技巧的简单代码;我重视搜索引擎、快速访问文档以及帮助我推理代码和导航项目的工具,而无需将所有内容都记在脑子里。
我们不应该为现在的自己编写代码,而应该为几年后的自己编写代码。思考是困难的,而编程需要大量的思考,即使不需要破译棘手或不清楚的代码。
开始思考:
如果您有任何反馈,请发送给我、发推文、在 GitHub 上提出问题,或发送电子邮件至 artem@sapegin.ru。获取您的副本。
以上是清洗你的代码:别让我思考的详细内容。更多信息请关注PHP中文网其他相关文章!