我开发了一个基于 Beancount 的账本托管服务 HostedBeans,欢迎大家来了解纯文本复式记账并试用我的服务。

论代码中为什么不应当写注释

当很多前辈教育后辈应当多写注释的时候,当网络上充满了有关程序员从不写注释的段子的时候,这是一个非常有争议的话题。作为一个标题党,容我先修正一下我的观点:我认为如果代码写得足够好,那么大多数注释是多余的,我们应该通过写出更好的代码来代替更多注释。

注释的确有其用途,但大部分情况下,程序员在滥用注释。我是反对夹杂在代码间的注释的,我认为注释应当从代码中独立出来——通常被称为文档。

请看下面一段代码。

/* /static/market/checkout.js

2014.7.2 create by orzfly
2014.7.29 update by jysperm: fixbugs

TODO: 这段代码中注释太多了,需要移除一些 -- jysperm
*/

var raw_products = req.query['products'].split(',');

// 商品 ID 的数组
var products = []

// 过滤每个参数
for(var i = 0, i < raw_products.length, i++) {
    if (!raw_products[i])
        return;

    // 前端传来的数据中居然会有空格
    if (!raw_products[i].trim())
        return

    /* 2014.7.22: 现在可以使用非数字 ID 了
    // 略过非数字条目
    if (isNan(raw_products[i].trim().toFixed()))
        return;
    */

    products.push(raw_products[i].trim().toFixed());
}

// 总钱数
var sum = 0;

// 计算每个商品的总钱数
for(var i = 0, i < products.length, i++) {
    // 从数据库中查商品信息
    var data = db.product.byID(products[i]);

    // TODO: 谁来写一下没查到商品的情况

    // 把商品的价格加到总钱数上, a += b 是 a = a + b 的缩写
    sum += data.price;
}

你居然花了一半的时间在读注释上面,这是多么浪费生命的事情,在代码中每加一行注释,都会增加代码的阅读成本——即使阅读者已经了解了注释所要传达的精神;同时也会增加维护成本:修改这段代码的人不得不连同注释一起修改——而且你不能确定他到底会不会这么做。

所以只有当非常必要的情况下,才应该添加注释,而且应当言简意赅。注释不应当解释一段代码在做什么,因为这是每个合格的程序员都应该知道的事情,而是应该解释这段代码为什么要这样做。

由此引出几种明显不应该添加的注释:

  • 本应由版本控制系统记录的信息、对代码的评论,以及不是很重要的 TODO.

    代码并不是全部,一个但凡靠谱一点的项目,都应当有自己的版本控制系统,除了记录代码差异之外,还应该有工单和 Issue 的功能。
    阅读代码的人通常不需要了解几个程序员之间的恩怨,很多时候也不关心这段代码的历史,这些信息只会把代码拖得越来越长。

  • 废弃的代码

    被弃用的代码应该被删掉,这些代码会非常影响阅读,而且它们一般又很长。
    在绝大多数情况下,被弃用的代码不会重新派上用场,即使出现了少数情况,你也可以从版本控制系统中找到它们。

  • 对变量和函数名的解释

    这种情况下显然你需要一个更恰当的名字,如果这个标识符有一个比较小的作用域,你可以使用一个比较长的名字以便容纳更多信息。

    例如上文中的:

    • products 应改为 products_id
    • sum 应改为 total_amount
    • data 应改为 product_record
  • 对语法的解释,以及显而易见的事情

    例如上文中的「把商品的价格加到总钱数上, a += b 是 a = a + b 的缩写」,这显然是任何一个人都知道的事情。

    也许有人愿意通过写这样的注释来梳理思路:

      // 过滤参数:
      //    去掉 ID 里的空格
      //    去掉非数字 ID
      // 循环每一个商品:
      //    去数据库查记录
      //    把商品的价格加到总钱数上
    

    但是当代码写完的时候记得删掉。

  • 对逻辑块的概括

    例如上文中的「过滤每个参数」和「计算每个商品的总钱数」,这情况下通常是你没有对逻辑进行抽象,具体表现就是像下面这样:

      // 首先有 25 行代码去做事情 A
      // 然后有 5 行代码去做事情 B
      // 这里有 90 行代码去做事情 C
      // 最后有 45 行代码去做事情 D
    

    这导致你需要一些注释来分割这四个部分。如果这四个部分都是一个函数调用的话,那么函数名本身就是对逻辑的一种解释,读者可以快速地找到函数 B, 而不必在前 25 行中搜索做事情 B 的五行代码。

综上,我对这段代码的改善意见如下:

var filterProductID = function(raw_products_id) {
    result = []

    raw_products_id.forEach(function(product_id) {
        if (product_id and product_id.trim())
            products_id.push(product_id.trim().toFixed());
    });

    return result;
};

var getPriceOfProduct = function(id) {
    var product_record = db.product.byID(products[i]);

    if (product_record)
        return product_record.price;
    else
        return 0;
};

var products_id = filterProductID(req.query['products'].split(','));
var tatol_amount = 0;

products_id.forEach(function(product_id) {
    tatol_amount += getPriceOfProduct(product_id);
});

虽然我在以一段虚构的,刻意编造的代码来佐证我的观点,但我相信在实际的项目中,同样可以通过改善代码来减少注释,而且总体上来讲会节约更多的时间和精力。

有关 RP3 的发布计划和注意事项(8 月 11 日)

RP3 发布计划

因为 jp1 的用户量较少(已经关闭注册半年多了), RP3 将先在 jp1 上线,jp1 的用户会被强制升级至 RP3, 有关升级过程中的注意事项见下文。

RP3 中,我们决定只使用 Linode 的服务器,因此 us1 会关闭注册,建议上面的用户自行迁移至稍后加开的 us2 节点。在三个月后 us1 会被关闭,如果届时还有用户没有迁移至 us2, 我们会向用户的支付宝帐号退款(剩余时长加额外 50% 的补偿), 并将数据发送至注册邮箱(数据托管在 Dropbox, 最早失效时间不早于 3 个月后).

因为 LocVPS 反复遭到攻击被机房下线,因此临时决定 us1 也在 8 月 11 日上线,8 月 11 日之前暂停服务,这是一个艰难的决定,所有受到影响的 us1 用户赠送 60 天使用时长。如果无法接受关闭半个月之久,只能找我要回数据并退款了。

总之:

  • jp1, us1 由 RP2 升级为 RP3 (8 月 11 日)

RP3 功能概要

RP3 基本包含了 RP2 的所有功能,价格调节到了 10 元 / 月,支持比特币支付。性能没有太大变化,从 RP3 起 RP 主机仅使用 Linode 作为服务器提供商。RP3 新建站点时支持「向导」、「JSON 配置文件」和「Nginx 配置文件」三种模式,可以非常灵活地设置 Nginx. RP3 新增了 MongoDB, Memcached, Redis 等数据库的支持,将 MySQL 换成了 MariaDB. 界面上 RP3 使用了 Bootstrap3 的扁平化 UI 风格,现在可以实时地显示资源占用率。RP3 基于 Node.js 和 MongoDB, 将以 GPL 开源,RP3 程序本身支持多套餐,支持简单地以插件的形式支持其他服务。

jp1 升级注意事项

你的 MySQL 数据库和 home 目录下的数据会被自动地转移,密码同原登录密码,其他信息可能会丢失(如 crontab), 面板中的工单记录和日志日志会丢失,大部分非特殊的站点配置会被同步到新系统中,不能自动同步的会发邮件通知。

升级工作自 2014 年 8 月 11 日晚 10 点开始,可能持续 6 - 24 个小时,这段时间中你将无法使用 RP 主机。

us1 升级注意事项

us1 主机会停止运行,直到 8 月 11 日更新 RP3, 数据会被自动的迁移,密码同原登录密码,特殊情况会发邮件通知。

升级工作自 2014 年 8 月 11 日晚 24 点开始,可能持续 6 - 24 个小时

其他

所有受到此次升级影响的用户赠送 30 天使用时长,本文的副本会以邮件的形式发给 RP 主机的所有用户。

未来的互联网安全体系

这个博客创建于精子 7 岁第一天上学的时候,之后的十几年里精子的成长和价值观的改变是很大的。
所有的日志发布之后除了错别字都不会再修改,因此他可能并不完全同意早期日志中的观点。

前两天在撰写 RP 主机的「服务条款」,参考了几家知名的 VPS 和虚拟主机服务提供商(如 Linode)的服务条款,发现他们无一例外地对「滥发垃圾信息」非常在意。由此想到,现在很多邮件服务提供商,在以非常野蛮的方式来进行反垃圾邮件工作,例如直接封禁某些发出大量垃圾邮件的 IP, 所以 Linode 才会如此认真地保护他们的 IP 资源。

基于 IP 的反垃圾邮件策略显然是不合理的,那么让我来想象一下未来的互联网安全体系是怎样的。首先提示各位读者,阅读本文需要你对公钥加密算法有一个基本的了解。而且碰巧这里有一篇我以前写过的有关公钥加密算法的日志:http://jysperm.me/technology/1074.

我认为随着越来越多的基础服务依赖于互联网,人们会越来越重视网络安全,未来的 IP 协议(也许是 IPv7 或者一个其他的什么版本号), 应当强制对每一个数据包进行加密。网络上的每个终端都有一个密钥对,就好像 MAC 地址一样,出厂既有,当然,也可以自行更换。所有 IP 数据包都以对方的公钥来加密,同时以自己私钥来签名,这样便在 IP 层实现了端到端加密,和基于数字签名的身份鉴定。

公钥就是一个网络终端的身份证,权限认证可以直接基于公钥来进行,例如 IP 白名单会变成公钥白名单,邮件服务提供商也会依据公钥来封禁恶意的终端。甚至很多软件和网站也不再需要设置密码了,只要授权自己的设备的公钥以访问权限即可。

生成一个密钥对是十分简单的,所以这无法阻止一些恶意的用户通过生成大量的密钥对逃避封禁。因此,我认为生成一个密钥对应当是有成本的,需要支付现实世界的货币才可以。即类似于现在的 SSL 证书的模式,会有经过权威认证的证书(密钥对)颁发机构来颁发密钥对,每生成一个新的密钥对都要向颁发机构支付一定费用(例如 100 人民币), 这在全世界范围内会是一笔相当大的费用,可以考虑直接用这笔费用来维护核心路由等网络设施,于是大家接入互联网就不需要额外的费用了。

这个密钥对就是一个网络终端(通常也属于一个人或者机构)的身份证,代表着它在网络上的行为,例如发送垃圾邮件,对其他终端进行攻击等,都会被无可抵赖地记录在案。当一个密钥对的恶意行为过多时,执法机构可能会吊销这个密钥对,就如同现在吊销一个 SSL 证书一样。租用服务器的时候,提供商会要求你自行提供密钥对,服务提供商再也不必担心因为滥发垃圾信息而导致 IP 被封禁了。普通人是不需要频繁更换密钥对的,只有那些尝试破坏规则的恶意用户,才会经常被吊销密钥对。

可以想象,有些国家会要求对密钥对进行实名制管理,非实名的密钥对签名的数据包,不允许其通过骨干路由,这种方案会比模糊不清的封禁更加有效地管理互联网秩序。这样也带来了一个好处,就是密钥对会像实名制的手机卡一样,只要凭身份证明就可以方便地挂失,也不必去为各种网站和软件设置密码,因为这些帐号已经通过密钥对被绑定到你的名下了,只要通过身份证明就可以随时找回这些帐号。

多么美好的未来。

精子生于 1995 年,英文 ID jysperm.

订阅推送

通过 Telegram Channel 订阅我的博客日志、产品和项目的动态:

王子亭的博客 @ Telegram


通过邮件订阅订阅我的博客日志、产品和项目的动态(历史邮件):

该博客使用基于  Hexo  的  simpleblock  主题。博客内容使用  CC BY-NC-ND  授权发布。最后生成于 2025-02-10.