编写干净、易于理解且可维护的代码是每位开发者必须掌握的核心技能。这不仅能提升个人效率,更能为团队协作扫清障碍。本文将深入探讨提升代码质量的关键原则,并通过具体的代码示例,带你逐步掌握编写整洁代码的实用技巧。
如何命名变量(以及其他事物)
我们使用变量名而非内存地址的根本原因在于:名称承载了信息。一个好的名字能清晰地传达其用途,让他人(包括未来的你)一目了然。
找到一个贴切的名字或许会多花几分钟,但这将在代码的整个生命周期中为你和你的团队节省数小时甚至数天的时间。相信许多开发者都有过这样的体验:几个月后回顾自己的代码,却难以理解当初的意图。
如何创建有意义的名字
不要依赖注释来解释一个变量的用途。如果一个名字晦涩到需要注释来说明,那么你真正应该做的是花时间重命名它,而不是撰写注释。
差:
var d; // elapsed time in days
我见过无数次这样的代码。许多人误以为注释可以弥补糟糕命名的缺陷,但这是一种常见的误解。除非有特殊理由(例如简单的循环计数器),否则应避免使用 x, y, a, b 这类单字母作为变量名。
好:
var elapsedTimeInDays;
var daysSinceCreation;
var daysSinceModification;
这些名称就好得多,它们明确指出了被测量的对象以及所使用的单位。
避免错误信息
注意那些具有特定技术含义的词汇。例如,除非变量类型确实是 List,否则不要将一组账户命名为 accountList。这个词会让人误以为它是一个特定的数据结构。即使它的类型是列表,accounts 也是一个更简洁、更好的选择。
差:
var accountList = [];
好:
var accounts = [];
避免噪音词
噪音词是指那些没有提供任何额外信息、纯属冗余的词语。它们应当被删除。
一些常见的噪音词包括:
- The (作为前缀)
- Info
- Data
- Variable
- Object
- Manager
如果你的类名是 UserInfo,直接叫 User 就足够了。同样,用 BookData 代替 Book 也是多此一举,因为类的本质就是封装数据。
使用发音友好的名称
如果你无法流利地读出一个名字,那么在团队讨论中提及它时会显得非常尴尬。
差:
const yyyymmdstr = moment().format("YYYY/MM/DD");
好:
const currentDate = moment().format("YYYY/MM/DD");
使用可搜索的名称
避免在代码中直接使用“魔法数字”。应该使用有意义的、可搜索的命名常量。不要使用单字母名称作为常量,因为它们可能出现在代码的许多地方,难以通过搜索定位。
差:
if (student.classes.length < 7) {
// Do something
}
好:
if (student.classes.length < MAX_CLASSES_PER_STUDENT) {
// Do something
}
后一种方式更优,因为 MAX_CLASSES_PER_STUDENT 可以在代码中多处复用。如果未来需要将这个限制改为 6,你只需在一个地方修改常量值即可。而糟糕的例子则会在读者心中留下疑问:这个数字 7 到底代表什么?
你也应该遵循所用语言的常量声明约定,例如 Java 中的 private static final 或 JavaScript 中的 const。深入理解这些编程语言的基础规范是写出专业代码的前提。
保持一致性
坚持“一个概念对应一个词”的原则。不要在不同的类中对相同的操作混用 fetch、retrieve 和 get。从中选择一个,并在整个项目中保持一致。这样,无论是维护代码的同事还是使用你 API 的客户,都能轻松找到他们想要的方法。
如何编写函数
保持它们简短
函数应该非常短小。它们很少需要超过20行。一个函数越长,它就越可能承担了过多的职责并产生意料之外的副作用。
确保它们只做一件事
这是函数设计的黄金法则:一个函数只应完成一项明确的任务。遵循这一原则,你的函数自然会变得短小精悍。函数所做的所有事情,都应该能够从其名字中清晰地体现出来。
有时,我们很难从函数名直接判断它是否做了多件事。一个有效的检验方法是:尝试从该函数中提取出另一个不同名称的函数。如果你能成功提取,那就说明原函数应该被拆分。
这可能是本文中最重要、也最需要时间适应的概念。但一旦你掌握了它,你的代码将显得更加成熟,并且重构、理解和测试起来也会容易得多。
将条件语句封装在函数中
重构复杂的条件判断,并将其放入一个命名良好的函数中,是提升条件语句可读性的绝佳方法。
以下是我曾经在一个学校项目(Connect4游戏)中编写的代码片段,负责在棋盘上插入棋子。isValidInsertion 方法封装了关于列号有效性的所有检查,让我们可以专注于插入棋子的核心逻辑。
public void insertChipAt(int column) throws Exception {
if (isValidInsertion(column)) {
insertChip(column);
boardConfiguration += column;
currentPlayer = currentPlayer == Chip.RED ? Chip.YELLOW : Chip.RED;
} else {
if (!columnExistsAt(column))
throw new IllegalArgumentException();
else if (isColumnFull(column - 1) || getWinner() != Chip.NONE)
throw new RuntimeException();
}
}
isValidInsertion 方法的实现如下:
private boolean isValidInsertion(int column) {
boolean columnIsAvailable = column <= NUM_COLUMNS && column >= 1 && numberOfItemsInColumn[column - 1] < NUM_ROWS;
boolean gameIsOver = getWinner() != Chip.NONE;
return columnIsAvailable && !gameIsOver;
}
试想一下,如果没有这个封装方法,if 条件将会变成这样:
if (column <= NUM_COLUMNS
&& column >= 1
&& numberOfItemsInColumn[column - 1] < NUM_ROWS
&& getWinner() != Chip.NONE)
是不是看起来就很混乱?我完全同意。
减少参数数量
函数的参数应尽可能少,最好不超过两个,尽量避免三个或更多参数。参数越多,函数就越难阅读和理解。从测试的角度看,参数过多的函数也更难进行完备的测试,因为你需要为各种参数组合编写大量的测试用例。
不要使用标志参数
标志参数是指传递给函数的一个布尔值,函数根据这个值的不同而执行不同的逻辑。例如,假设有一个负责预订音乐会门票的函数,用户有高级和普通两种类型:
public Booking book (Customer aCustomer, boolean isPremium) {
if(isPremium)
// logic for premium book
else
// logic for regular booking
}
标志参数天生就违反了函数的单一职责原则。当你看到它们时,应该考虑将函数拆分成两个独立的函数。
不要产生副作用
副作用是指代码产生的、超出其声明功能之外的额外影响。例如,修改了按引用传递的参数,或者更改了某个全局变量。
关键在于,这些副作用往往隐藏在函数承诺的主要功能之下,你需要仔细阅读代码才能发现它们,这极易引发难以追踪的 Bug。
看看下面这个例子:
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
你能发现这个函数的副作用吗?它在检查密码,但当密码验证通过时,它还会初始化用户会话(Session.initialize()),这就是一个隐蔽的副作用。
你或许可以把函数名改为 checkPasswordAndInitializeSession 来显式说明这个效果。但当你这样做时,你应该意识到这个函数实际上在处理两件不同的事。更合理的设计应该是将初始化会话的逻辑分离出去。
不要重复你自己 (DRY)
代码重复堪称“万恶之源”。重复的代码意味着,当业务逻辑发生变化时,你需要在多个地方进行修改,这极易导致遗漏和错误。每当你在代码中看到重复的模式时,请立即使用 IDE 的重构功能将其提取为独立的方法或函数。
其他重要的整洁原则
不要在代码注释中留下被注释掉的代码
千万不要这样做!这一点非常严重,因为其他看到这段代码的人会因不确定它是否还有用而不敢删除它。于是,这些被注释掉的代码会像僵尸一样长期存在。随着变量名或方法名的变更,它们会逐渐变得过时且无关,但依然没人清理。
最好的做法是直接删除它。如果它真的重要,版本控制系统(如 Git)会帮你记录历史,随时可以找回。
了解并遵守你所用语言的规范
你应该熟悉你所使用编程语言在代码风格、空格、注释和命名等方面的约定。许多主流语言都有官方或社区认可的风格指南。
例如,在 Java 中通常使用驼峰命名法 camelCase,而在 Python 中则使用蛇形命名法 snake_case。在 C# 中,开括号 { 通常放在新的一行,而在 Java 和 JavaScript 中,则习惯与语句放在同一行。这些规范因语言而异,没有统一标准。
结论
编写整洁的代码并非一朝一夕就能掌握的技能,而是一种需要持续实践、并将上述原则内化为本能的习惯。记住,你写下的每一行代码,未来都可能被他人(包括你自己)阅读和修改。从为变量起个好名字开始,到设计精巧的函数,每一步都是对代码质量的投资。
希望这篇指南能为你迈向优秀开发者的道路提供清晰的指引。技术之路漫长,持续学习和交流至关重要。欢迎在云栈社区与更多开发者分享你的实践与心得。