[译] 像高手一样编写代码:编写代码的 5 个必知技巧
原文地址:https://www.mensurdurakovic.com/write-code-like-a-senior-five-must-know-tips-for-crafting-code/
在本文中,我们将讨论 5 个简单的技巧,你可以从此刻开始使用它们来提高代码质量。
无论你使用哪种编程语言,也无论你有多少年的编程经验,你都可以在日常工作中使用这些技巧。
那么,让我们开始吧。
嵌套和提前返回
毫无疑问,每个人都会在自己的项目中遇到一些大量的嵌套代码。
嵌套代码是指一个代码块包含另一个代码块的代码结构。这通常会导致多级缩进。虽然嵌套代码并非坏事,但过度嵌套会导致一些问题。这些问题会增加代码的维护和调试难度。
嵌套之所以会产生问题,是因为:
- 复杂性和可读性:嵌套代码很难阅读
- 维护:作为第一点的副作用,嵌套代码更难维护
- 调试:嵌套代码更难调试
- 范围问题:变量经常重叠,并导致作用域问题
- 测试:为大量的嵌套代码编写测试比较困难
请看这段代码片段:
if (user && user.isAuthenticated) {
// ...20 lines of code
if (timezone) {
// ...10 lines of code
if (isQualified) {
// ...50 lines of code
}
}
}
应该怎么优化呢?
const userAuthenticated = user && user.isAuthenticated;
if (!userAuthenticated) {
return;
}
// ...20 lines of code
const timezone = ...
if (!timezone) {
return;
}
// ...10 lines of code
const isQualified = ...
if (!isQualified) {
return;
}
// ...50 lines of code
你看这样多好。没有缩进,一切都更容易阅读。
与嵌套有关的另一个概念是提前返回。
提前返回是一种编程概念,即函数在完成代码之前退出并返回一个值。这通常用于处理特殊情况或错误条件。
让我们来看看这个例子:
function getDailyRewards(user){
if(user.isAuthenticated){
// ...50 lines of code
}
}
这个 if 条件包装了约 50 行代码。丑得要命。让我们重写它:
funcion getDailyRewards(user) {
if (!user.isAuthenticated) {
return;
}
// ...50 lines of code
}
看起来好多了。
如果用户未通过身份验证,我们就提前退出,而不是用 if 条件来封装整个代码逻辑。代码的其余部分都在防卫语句 guard clause
下面,阅读起来更方便。
无用的 if/else 块
这一条是专门针对初学者的。当学生在代码中写入无用的 if/else 块时,我喜欢在练习中对他们进行提示。
这里有个典型的例子:
function isEven(number) {
if (number % 2 === 0) {
return true;
} else {
return false;
}
}
当我看到这样的代码时,我会问他们能不能写得更短、更优雅一些。然后,有些人就会这样做:
function isEven(number) {
if (number % 2 === 0) {
return true;
}
return false;
}
然后,我问他们能否将这 4 行写成 1 行。然后他们会这样做:
function isEven(number) {
return number % 2 === 0 ? true : false;
}
我再次询问他们是否可以不使用 if/else 或三元运算符,最后我们得到了这样的结果:
function isEven(number) {
return number % 2 === 0;
}
下面还有一个与第一种情况类似的例子。
function getScheduleKey() {
const hasSchedule = this.hasTeleVisitScheduled();
if (hasSchedule) {
return this.schedule.key;
} else {
return this.schedule.cancelSchedule.key;
}
}
如果仔细观察,这里不需要写 else 块。反正函数已经结束,也有返回。
因此,我们可以这样解决:
function getScheduleKey() {
const hasSchedule = this.hasTeleVisitScheduled();
if (hasSchedule) {
return this.schedule.key;
}
return this.schedule.cancelSchedule.key;
}
这个故事的寓意是:避免使用无用的 if/else 块。
硬编码的字符串和数字
硬编码字符串和数字可在代码中嵌入值。而不是使用变量或常量来表示它们。
硬编码值通常被认为是不好的做法,这有几个原因:
缺乏灵活性和可维护性:如果在整个代码库中都对值进行硬编码,要对这些值进行更改,就需要找到并修改每一次出现的值。这意味着,如果你在代码中对一个字符串进行了 100 次硬编码,而 2 个月后你需要修改该字符串,那么你需要在 100 个地方再次修改该字符串。
可重用性:硬编码值很难在不同情况下重用相同的代码。如果想在不同的值中使用相同的逻辑,就需要复制代码。然后,你可以更改值,这就导致了代码的重复。
可读性:带有硬编码值的代码会变得更难阅读和理解。尤其是对于不熟悉代码的其他开发人员来说。有意义的变量名会让值的目的更加明确。
调试和错误检测:当出现错误时,如果使用命名的变量或常量,就更容易发现错误。
可扩展性:随着项目的增长,维护硬编码值变得非常具有挑战性。在整个代码库中更改一个值可能会变得繁琐且容易出错。
本地化和国际化:如果你的代码需要支持多种语言或本地化,硬编码值会使翻译变得困难。
总之,在代码中使用硬编码字符串和数字是不好的。
现在,让我们来看看这个精美的示例,它激发了我写这篇文章的灵感。这个例子的原版要长得多,但现在的版本更简短,减少了 if 条件:
if (fitnessTrainingSchedule.name == "Training 03 Schedule") {
checkWindowForTrainingSchedule(10, 16, "Milestone3Schedule", "Training 03 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 04 Schedule") {
checkWindowForTrainingSchedule(24, 30, "Milestone4Schedule", "Training 04 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 05 Schedule") {
checkWindowForTrainingSchedule(52, 58, "Milestone5Schedule", "Training 05 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 06 Schedule") {
checkWindowForTrainingSchedule(80, 86, "Milestone6Schedule", "Training 06 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 07 Schedule") {
checkWindowForTrainingSchedule(108, 114, "Milestone7Schedule", "Training 07 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 08 Schedule") {
checkWindowForTrainingSchedule(136, 142, "Milestone8Schedule", "Training 08 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 09 Schedule") {
checkWindowForTrainingSchedule(164, 170, "Milestone9Schedule", "Training 09 Schedule");
}
if (fitnessTrainingSchedule.name == "Training 10 Schedule") {
checkWindowForTrainingSchedule(216, 230, "Milestone10Schedule", "Training 10 Schedule");
}
乍一看,你可以看到一堆相似的数字和字符串,但你根本不知道这些代码在做什么。你只知道它在做与健身训练时间表有关的事情。要理解这段代码,你需要看看其他脚本。
让我们看看重构后的版本:
const TRAINING_SCHEDULE_03 = {
name: "Training 03 Schedule",
milestone: "Milestone3Schedule",
startDaysInterval: 10,
endDaysInterval: 16
};
const TRAINING_SCHEDULE_04 = {
name: "Training 04 Schedule",
milestone: "Milestone4Schedule",
startDaysInterval: 24,
endDaysInterval: 30
};
const TRAINING_SCHEDULE_05 = {
name: "Training 05 Schedule",
milestone: "Milestone5Schedule",
startDaysInterval: 52,
endDaysInterval: 58
};
const TRAINING_SCHEDULE_06 = {
name: "Training 06 Schedule",
milestone: "Milestone6Schedule",
startDaysInterval: 80,
endDaysInterval: 86
};
const TRAINING_SCHEDULE_07 = {
name: "Training 07 Schedule",
milestone: "Milestone7Schedule",
startDaysInterval: 108,
endDaysInterval: 114
};
const TRAINING_SCHEDULE_08 = {
name: "Training 08 Schedule",
milestone: "Milestone8Schedule",
startDaysInterval: 136,
endDaysInterval: 142
};
const TRAINING_SCHEDULE_09 = {
name: "Training 09 Schedule",
milestone: "Milestone9Schedule",
startDaysInterval: 164,
endDaysInterval: 170
};
const TRAINING_SCHEDULE_10 = {
name: "Training 10 Schedule",
milestone: "Milestone10Schedule",
startDaysInterval: 216,
endDaysInterval: 230
};
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_03.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_03.startDaysInterval,
TRAINING_SCHEDULE_03.endDaysInterval,
TRAINING_SCHEDULE_03.milestone,
TRAINING_SCHEDULE_03.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_04.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_04.startDaysInterval,
TRAINING_SCHEDULE_04.endDaysInterval,
TRAINING_SCHEDULE_04.milestone,
TRAINING_SCHEDULE_04.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_05.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_05.startDaysInterval,
TRAINING_SCHEDULE_05.endDaysInterval,
TRAINING_SCHEDULE_05.milestone,
TRAINING_SCHEDULE_05.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_06.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_06.startDaysInterval,
TRAINING_SCHEDULE_06.endDaysInterval,
TRAINING_SCHEDULE_06.milestone,
TRAINING_SCHEDULE_06.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_07.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_07.startDaysInterval,
TRAINING_SCHEDULE_07.endDaysInterval,
TRAINING_SCHEDULE_07.milestone,
TRAINING_SCHEDULE_07.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_08.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_08.startDaysInterval,
TRAINING_SCHEDULE_08.endDaysInterval,
TRAINING_SCHEDULE_08.milestone,
TRAINING_SCHEDULE_08.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_09.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_09.startDaysInterval,
TRAINING_SCHEDULE_09.endDaysInterval,
TRAINING_SCHEDULE_09.milestone,
TRAINING_SCHEDULE_09.name
);
}
if (fitnessTrainingSchedule.name == TRAINING_SCHEDULE_10.name) {
checkWindowForTrainingSchedule(
TRAINING_SCHEDULE_10.startDaysInterval,
TRAINING_SCHEDULE_10.endDaysInterval,
TRAINING_SCHEDULE_10.milestone,
TRAINING_SCHEDULE_10.name
);
}
这段代码无疑更长,但更易读、易懂。你可以看到这段代码在做什么,参数的含义是什么。
当然,这段代码远非完美。它包含了很多重复的代码,我们下一步要修正这些代码。
重复代码
代码重复也称为代码冗余。它是指在一个项目中的许多地方出现相同或相似的代码块。在编程中,代码重复通常被认为是不好的,原因有以下几点:
- 维护复杂:当相同的代码出现在许多地方时,对该代码的任何更改或更新都需要在所有这些地方进行。这增加了引入错误的风险。这也增加了维护的难度和时间。
- Bug 扩散:如果重复代码中存在错误,则需要在每个实例中进行修复。漏掉哪怕一个实例都可能导致不一致的行为或意想不到的问题。
- 可读性和理解性:重复代码会使代码库更难阅读和理解。特别是对于可能需要处理或维护代码的其他开发人员来说。它掩盖了项目的逻辑和意图。
- 代码膨胀:重复代码会导致代码臃肿,使项目变得更大更慢。这会影响性能和效率。
- 代码审查和协作:在代码审查过程中,冗余代码会被标记为问题。这将引发有关代码组织和设计选择的讨论。这也会在版本控制系统中产生冲突。特别是当许多开发人员都在开发类似的代码时。
我们之前重构的代码示例有一个很大的问题,那就是代码重复。它违反了 DRY 原则,即“不要重复自己”。
让我们来解决这个问题:
const TRAINING_SCHEDULES = [
{
name: "Training 03 Schedule",
milestone: "Milestone3Schedule",
startDaysInterval: 10,
endDaysInterval: 16
},
{
name: "Training 04 Schedule",
milestone: "Milestone4Schedule",
startDaysInterval: 24,
endDaysInterval: 30
},
{
name: "Training 05 Schedule",
milestone: "Milestone5Schedule",
startDaysInterval: 52,
endDaysInterval: 58
},
{
name: "Training 06 Schedule",
milestone: "Milestone6Schedule",
startDaysInterval: 80,
endDaysInterval: 86
},
{
name: "Training 07 Schedule",
milestone: "Milestone7Schedule",
startDaysInterval: 108,
endDaysInterval: 114
},
{
name: "Training 08 Schedule",
milestone: "Milestone8Schedule",
startDaysInterval: 136,
endDaysInterval: 142
},
{
name: "Training 09 Schedule",
milestone: "Milestone9Schedule",
startDaysInterval: 164,
endDaysInterval: 170
},
{
name: "Training 10 Schedule",
milestone: "Milestone10Schedule",
startDaysInterval: 216,
endDaysInterval: 230
}
];
const targetedTrainingSchedule = TRAINING_SCHEDULES.find(
(trainingSchedule) => trainingSchedule.name === fitnessTrainingSchedule.name
);
if (!targetedTrainingSchedule) {
return;
}
checkWindowForTrainingSchedule(
targetedTrainingSchedule.startDaysInterval,
targetedTrainingSchedule.endDaysInterval,
targetedTrainingSchedule.milestone,
targetedTrainingSchedule.name
);
正如你所看到的,我们将所有对象移到了一个数组中。然后,我们不再对每一个可能的健身训练计划重复 if 条件,而是找到我们要找的那个。然后,我们将目标训练计划的参数传递到函数中。
这样看起来比以前漂亮多了。
函数参数混乱
函数实参,又称函数形参(注:其实都可以统称为函数参数,没必要过度比较),是在调用函数时提供给函数的值。这些参数为函数执行任务或计算提供了必要的数据。
如果不认真对待函数参数的使用,就会在代码中造成混乱。我曾经遇到过这样一个函数:
function createUser(
username,
email,
password,
firstName,
lastName,
birthdate,
gender,
phoneNumber,
profilePicture,
registerPurpose,
isActive,
isAdmin,
isVerified,
registrationDate,
status,
timezone,
theme,
accountType,
subscriptionPlan,
preferredLanguage,
expiryDate,
lastPasswordUpdate,
lastLogin,
notificationSettings,
customFields
)
当一个函数有大量参数时,其理解、阅读和维护就会变得更加困难。要跟踪每个参数的作用以及它们之间的相互作用,都是一项挑战。
包含大量参数的函数会使代码更难阅读和理解。尤其是对于新加入代码库的开发人员。
还有就是,测试带有多个参数的函数也会变得更加复杂。每个参数都可能有不同的值和交互作用。这就增加了涵盖函数行为所需的测试用例数量。此外,调试带有多个参数的函数中的问题也更具挑战性。
具有多个参数的函数可能与其调用者紧密耦合。如果一个参数发生变化,可能会影响函数的行为,需要在很多地方进行更改。
因此,上面这种情况后来被重构成了这样:
function createUser(userData)
这比以前好多了。
此外,在前面的示例中,函数从一个对象接收数据。没有必要逐个传递所有数据。你可以传递整个对象:
// this is not OK
checkWindowForTrainingSchedule(
targetedTrainingSchedule.startDaysInterval,
targetedTrainingSchedule.endDaysInterval,
targetedTrainingSchedule.milestone,
targetedTrainingSchedule.name
);
// this is OK
checkWindowForTrainingSchedule(targetedTrainingSchedule);
总之,好的做法是使用参数数量合理的函数。
如果发现需要很多参数才能实现某种功能,可以考虑使用对象等数据结构。对象允许你将相关参数组合在一起。你还可以将函数分解为更小更集中的函数。这些函数可以相互影响,以实现所需的效果。
这样可以使代码更简洁、更易于维护和测试。