//TODO:写一个更好的注释
评论带来的风险
避免不必要的评论
撰写有用的评论
结论
这是在 2019 年纽约 Droidcon 上展示的。请点击此处观看视频:
本月初,Java 官方推特账号发布了一条非常有争议的推文,告诉你不要再写代码注释。
它链接到一篇Medium 文章,该文章解释了代码注释的一些问题,并建议解决方案是完全停止编写它们。
这真是个糟糕的建议。我很震惊,这竟然出自如此流行编程语言的官方社交媒体账号。代码注释可以为你的代码库提供巨大的价值,并真正帮助到下一个继承你代码的人。我还发现这篇文章有些过于苛刻:
当你需要写注释时,通常意味着你写的代码表达能力不够强。每次写注释的时候,你都会感到胃里一阵剧痛。
我在这里想告诉你,写评论并不意味着你失败了,也并不意味着你应该为此感到内疚。作为一个社区,我们不应该停止写评论。我们需要学习如何写出更好的评论。我希望这篇文章能成为你在这方面的指南。
评论带来的风险
首先,让我们对评论给我们带来的好处以及风险有一个基本的了解。
注释很有用,因为它们可以为代码的读者提供代码本身可能无法提供的见解。
注释存在潜在风险,因为它们可能会过时。修改代码并不保证注释也会随之更改,因此注释可能会误导我们。因此,默认情况下,我们应该尽量避免使用注释。
然而,我们不应该为了回避而回避评论。我们应该确保我们有意地去写评论,或者不写评论。
根据我的经验,我遇到的代码注释可以分为三类:
- 此评论没有必要。
- 这条评论毫无帮助。
- 这条评论很有帮助。
我们的目标是确保所有评论都属于第三类——有用的评论。我们可以先删除所有不必要的评论,然后确保剩余的评论对读者有帮助。
避免不必要的评论
有些代码注释根本就没必要。在这种情况下,我们应该直接删除它们。否则,我们的文件会变得更加混乱,注释也更有可能过时,最终从不必要变为无用。
删除多余的评论
我们可以删除的第一种不必要的注释是多余的注释。
你可能很想为某个方法及其所有参数添加注释,但有时你的代码表达力太强,很难写出一条独特的注释。这时,你可能会想到这样的例子:
interface AccountDAO {
/**
* Inserts an account into the database.
*
* @param[account] The account that we're inserting.
* @return The ID of the inserted account.
*/
suspend fun insert(account: Account): Long
}
关于 insert 方法的文档并非必需。第一行提供了一些信息,但根据方法名称和它所属的接口,可以推断出它的作用。account 参数的解释再次重复了代码中已经提到的内容。不过,return 语句可以提供一些帮助。
所以这条注释的三分之二提供了代码中已经提供的信息。这意味着它是多余的。我认为没有注释比有多余的注释更好,因为这样可以避免注释过时的风险。所以我建议只包含有用的部分:
interface AccountDAO {
/**
* @return The ID of the inserted account.
*/
suspend fun insert(account: Account): Long
}
例外
此规则的一个例外是,如果您正在构建一个供他人使用的公共 API 或库。在这种情况下,您应该记录所有可用的内容。即便如此,我们仍然可以仔细考虑所写的注释,并确保它们有用,这一点我们稍后会讨论。
修改代码以避免需要注释
让我们继续实现避免注释的基准目标。你已经开始编写注释了,并且确保它不是多余的。接下来,你应该看看是否可以重写代码,使这条注释变得不再必要,以便将其删除。
下面是一个简短的单行注释的示例,有助于阐明方法可以做什么:
// Saves data to database
fun saveData() {
}
这条注释的作者(剧透:我)觉得这条注释有助于阐明该方法的作用。事后看来,我完全可以通过改一个更具表现力的方法名来避免这条注释:
fun saveDataToDatabase() {
}
另一个常见的例子是,当我们编写一个执行多项操作的方法时,我们想要分解该方法的各个部分,如下所示:
fun transferMoney(fromAccount: Account, toAccount: Account, amount: Double) {
// create withdrawal transaction and remove from fromAccount
// ...
// create deposit transaction and add from toAccount
// ...
}
且不论让一个方法执行多项操作是否违反了其他最佳实践,这还会导致我们产生一些额外的注释,而这些注释正是我们想要避免的。避免它们的一种方法是将每个步骤放入各自命名合适的方法中。
fun transferMoney(fromAccount: Account, toAccount: Account, amount: Double) {
withdrawMoney(fromAccount, amount)
depositMoney(toAccount, amount)
}
撰写有用的评论
如果您已经阅读完最后一节,并且仍然觉得应该包含您的评论,因为它是一个公共 API,或者因为您觉得它提供了额外的价值,我们需要问自己它是否对读者有帮助。
注释告诉你为什么,代码告诉你什么
我不记得第一次听到这句话是在哪儿,但它一直引起我的共鸣。代码应该告诉你发生了什么,而注释可以告诉你为什么。
让我们看看我最近写的一条糟糕的评论,它重复了以下内容:
/**
* A list of updated questions to be replaced in our list by an interceptor.
*/
private val updatedQuestions: MutableMap<Long, Question> = HashMap()
这条注释几乎没什么用,但我必须把它归到无用类别。它提供了一些额外的信息,但很大程度上重复了代码中已经能告诉我的内容。上一节我们说过应该删除这些注释,但这条注释背后的原因实际上是我想解释一下为什么我添加了这个 HashMap。
让我们把这个评论变成一个有用的评论,重点关注原因:
/**
* The `PagedList` class from Android is backed by an immutable list. However, if the user answers
* a question locally, we want to update the display without having to fetch the data from the network again.
*
* To do that, we keep this local cache of questions that the user has answered during this app session,
* and later when we are building the list we can override questions with one from this list, if it exists,
* which is determined based on the key of this HashMap which is the question ID.
*/
private val updatedQuestions: MutableMap<Long, Question> = HashMap()
现在我们有了一条注释,它实际上解释了我为什么要保留一个本地 HashMap,因为我想覆盖一个无法轻易修改的不可变列表(感谢 Android)。因此,我们得到了一条有用的注释。
带有示例的评论很有帮助
我们一直强调要避免冗余注释。但当我们创建一个公共 API 供他人使用时,如果希望确保所有内容都记录在案,那么就比较困难。这会带来一些重复信息的风险。以下是另一个例子:
class Pokedex {
/**
* Adds a pokemon to this Pokedex.
*
* @param[name] The name of the Pokemon.
* @param[number] The number of the Pokemon.
*/
fun addPokemon(name: String, number: Int) {
}
}
如果我们必须保留这些评论,我们会尽力确保它们有用。如果您无法提供更多信息,可以尝试提供示例:
class Pokedex {
/**
* Adds a pokemon to this Pokedex.
*
* @param[name] The name of the Pokemon (Bulbasaur, Ivysaur, Venusaur).
* @param[number] The number of the Pokemon (001, 002, 003).
*/
fun addPokemon(name: String, number: Int) {
}
}
这并没有显著改善,但我们没有向读者重复信息,而是向他们提供了一个预期的例子。
外部资源链接可能会有帮助
我们都曾在 StackOverflow 上找到过问题的解决方案。有时,我们可以轻松地复制粘贴到项目中;但有时,我们可能需要重用整个文件或方法。在这种情况下,读者可能会困惑,这段代码或概念的来源,或者为什么需要它。
您可以通过链接到相应的问题或答案来提供这些见解。最近,我借助 StackOverflow 创建了一个程序化的 ViewPager,我想在此给予赞扬,并确保如果 ViewPager 出现任何问题,我可以参考一些信息来获取更多信息:
/**
* A ViewPager that cannot be swiped by the user, but only controlled programatically.
*
* Inspiration: https://stackoverflow.com/a/9650884/3131147
*/
class NonSwipeableViewPager(context: Context, attrs: AttributeSet? = null) : ViewPager(context, attrs) {
}
可操作的评论才是好评论
这可能会让人感到震惊,因为评论通常不以可操作的形式呈现。可操作的评论很有帮助,因为它能给读者提供一些可借鉴的内容,避免他们问“我该如何处理这些信息?”
我认为我们可以关注两种类型的可操作的评论。
TODO 注释
总的来说,TODO 注释的风险很大。我们可能会看到一些我们想稍后再做的事情,于是就放弃了,//TODO: Replace this method
想着以后再回来做,但最终却永远也做不成了。
如果您要写 TODO 注释,您应该做以下两件事之一:
1) Just Do It™
2) 链接到您的外部问题跟踪器。
TODO 注释有一些有效的用例。也许您正在开发一个大型功能,并且想要发起一个只修复部分功能的拉取请求。您还想提出一些仍需完成的重构,但您将在另一个 PR 中修复。链接到一些可以督促您完成工作的内容,例如 JIRA 工单:
//TODO: Consolidate both of these classes. AND-123
这是可行的,因为它迫使我们去问题跟踪器创建工单。这比那些可能永远不会再看到的代码注释更不容易丢失。
弃用评论
另一种情况是,如果您对已弃用的方法或类留下评论,并且想要告诉用户下一步该去哪里,那么评论就可以具有可操作性。
我直接从 Android 中举了一个例子,他们弃用CoordinatorLayout.Behavior
了CoordinatorLayout.AttachedBehavior
:
/**
* ...
* @deprecated Use {@link AttachedBehavior} instead
*/
@Deprecated
public interface DefaultBehavior {
...
}
这条评论实际上很有帮助,因为它不仅告诉我某些东西已被弃用,还告诉我用什么来替代它。
结论
虽然注释存在风险,但并非所有注释都是坏事。重要的是,你要理解这种风险,并尽程序员应尽的责任来避免这种情况。具体来说,就是:
- 删除您实际上不需要的评论。
- 如果可以的话,重写代码以删除注释。
- 确保剩余的评论是有帮助的。
- 这是通过阐明原因、提供示例或采取行动来实现的。
有问题吗?请在评论区留言!您也可以在Twitter上轻松联系我。
文章来源:https://dev.to/adammc331/todo-write-a-better-comment-4c8c