今天和大家聊聊代码注释:
- 为什么很多人不喜欢写注释?
- 要不要写注释?
- 该怎么写注释?
现在的项目开发里,代码注释就像程序员的头发,越来越少。
尤其是国内,这种现象不仅是在小公司小团队中司空见惯,就算在大公司,以及大团队中的开源项目里,也是屡见不鲜。
上图是我在阿里的 Druid 项目源码里截的。DruidDataSource 是 Druid 重度使用的核心类,非常关键,可是哪怕这种关键的核心类,也见不到什么注释。
这张图则来自阿里的另一个著名开源项目Dubbo。DubboProtocol 是 dubbo 协议的实现类,而 dubbo 协议是 Dubbo 项目中最常见,使用最频繁的默认协议,一样没什么注释。
没有注释对我们读代码带来了很多的不便之处。就像扔给你一个数码产品,上面堆叠着密密麻麻的功能按键,但是却没有给你说明书。
那为什么代码注释消失了呢?
我尝试总结一下原因:
1. 国内程序员的职业环境对加注释不友好
在国内这种环境里,程序员们每天在苦闷的 996 中挣扎,各种大活小活不断地做着,正常写代码都忙得不可开交,加注释更是进一步提升了工作量,没人喜欢自己给自己加工作量的。
咱们想想,在费劲巴拉地写完一大堆代码之后,经过反复自测修改之后,好不容易调通了,脑子已经晕乎乎的了,你此时会有多大心思去写这段注释呢?
又再想想,你可能想着要给代码加注释呢,突然这边产品拉你开会,又或者那边运营告诉你,需求变了,刚写好的代码还得再改改……此时,你还有给代码加注释的念头吗?
另外,注释这事儿,写好了是很费精力的。一般来说,一段好的注释,要能在有限的行数之内说明出:被它注释的代码到底做了什么,是个怎样的概念以及为什么会写这段代码。
写注释麻烦不说,关键是注释还不算咱们程序员的工作量。
程序员的工作是把业务用程序实现,工作结果里不看你注释了多少代码,也不看你注释写的好还是坏,只看你的程序是不是写完了,满足了需求没有,会不会上线出什么问题。
至于注释,它滚出了程序员兄弟们的 KPI。有多少公司能像 Google 那样去 Review 代码的?BAT 有一个算一个,都差点意思。
所以,国内程序员们糟糕的环境,是代码注释少的首要原因。
2. 看待注释的方式出现了变化
Java 是一门面向对象的语言,从它出世以来,业界就不断地为 Java 制定了数不清的规范。
在 2008 年,集这些规范于大成的《Clean Code》—— 中文名叫《代码整洁之道》这本书出现了。
在《代码整洁之道》中有个理念就是,注释是为了弥补代码表达能力不足的一种不得已的做法。如果代码能表达清楚,那就没必要写注释。
甚至,这本书的作者认为写注释都需要用 failure 这个词来形容,也就是说,如果你写了注释,那就说明你的代码不够好,你写好代码的努力失败了。
这个理念在业界也被不少大牛们认可了。所以,后面就有越来越多的人认为:代码写的够好,就不用写注释了。
如果大家有空,可以去看看《代码整洁之道》的第四章,里面详细说明了这种如今被业界不少人接受的关于注释的理念。
所以,“好代码不需要注释”这种观点也是造成注释少的一个原因。
3. 注释没有规范,导致质量参差不齐
很多团队里,是没有注释规范的。对怎么注释,在哪里注释没有任何规定,随意程序员们自由发挥。
这就麻烦了,注释一旦写了,它就很关键了。因为
错误的注释,比没写注释还祸害人。
注释写的很差,那不仅没起到注释本应该起到帮助读代码的作用,反而还可能影响读代码,甚至还能把人带坑里。
如果没有注释规范,往往经常就会出现有人做的好有人做的差的情况。
比如,有人到处加注释,i = i + 1; //把i加1连这种简单代码都恨不得加注释,这就有点脱裤子放屁了。
还有的人写注释了,但是需求变了,代码改了之后,注释懒得改了。又或者是改代码的人不是原作者,新人改完之后压根就没意识到要改注释。
所以,如果没有规范,很多程序员对注释没有什么正确的概念,没写好注释由此还引来了埋怨……久而久之,就没人爱干加注释这件事了。
到底应该怎么写注释呢?
谈了那么多不写注释的原因,这里也想说明一下我对注释的观点。
我个人并不怎么赞同《代码整洁之道》对注释的观点,我自己读有好注释的代码,直接就省了五成以上的力气。有好注释的代码读起来,就像常年脑血栓,一朝被皮搋子打通了一样,那叫一个顺畅。
比如,看看 Netty 的注释:
- /**
- * A nexus to a network socket or a component which is capable of I/O
- * operations such as read, write, connect, and bind.
- * <p>
- * A channel provides a user:
- * <ul>
- * <li>the current state of the channel (e.g. is it open? is it connected?),</li>
- * <li>the {@linkplain ChannelConfig configuration parameters} of the channel (e.g. receive buffer size),</li>
- * <li>the I/O operations that the channel supports (e.g. read, write, connect, and bind), and</li>
- * <li>the {@link ChannelPipeline} which handles all I/O events and requests
- * associated with the channel.</li>
- * </ul>
- *
- * <h3>All I/O operations are asynchronous.</h3>
- * <p>
- * All I/O operations in Netty are asynchronous. It means any I/O calls will
- * return immediately with no guarantee that the requested I/O operation has
- * been completed at the end of the call. Instead, you will be returned with
- * a {@link ChannelFuture} instance which will notify you when the requested I/O
- * operation has succeeded, failed, or canceled.
- *
- * <h3>Channels are hierarchical</h3>
- * <p>
- * A {@link Channel} can have a {@linkplain #parent() parent} depending on
- * how it was created. For instance, a {@link SocketChannel}, that was accepted
- * by {@link ServerSocketChannel}, will return the {@link ServerSocketChannel}
- * as its parent on {@link #parent()}.
- * <p>
- * The semantics of the hierarchical structure depends on the transport
- * implementation where the {@link Channel} belongs to. For example, you could
- * write a new {@link Channel} implementation that creates the sub-channels that
- * share one socket connection, as <a href="http://beepcore.org/">BEEP</a> and
- * <a href="http://en.wikipedia.org/wiki/Secure_Shell">SSH</a> do.
- *
- * <h3>Downcast to access transport-specific operations</h3>
- * <p>
- * Some transports exposes additional operations that is specific to the
- * transport. Down-cast the {@link Channel} to sub-type to invoke such
- * operations. For example, with the old I/O datagram transport, multicast
- * join / leave operations are provided by {@link DatagramChannel}.
- *
- * <h3>Release resources</h3>
- * <p>
- * It is important to call {@link #close()} or {@link #close(ChannelPromise)} to release all
- * resources once you are done with the {@link Channel}. This ensures all resources are
- * released in a proper way, i.e. filehandles.
- */
- public interface Channel extends AttributeMap, Comparable<Channel> {
Channel 是 Netty 里非常核心的一个接口,你直接看注释,一下子就能理解了 Netty 为啥搞出个 Channel 类来,Channel 类你可以怎么玩儿,这些 Netty 在注释给你说得清清楚楚、明明白白。
所以,我觉得注释一定是要的,只是需要有个标准,也要有个度。
从实践上看,我们团队有这么几个必须加注释的标准:
1. 复杂的业务逻辑
业务逻辑关联太多的东西又或者步骤非常多,更或者两者兼有,那么就很少有人会去耐心仔细的去一行一行的把整个代码全部读通理顺。
这时候,必须在业务逻辑实现的相关类中,把类在业务逻辑实现中是个什么成分,为什么这么设计类,以及对应的业务逻辑都要讲清楚。并且重构代码后,注释也必须跟着重构。
2. 晦涩的算法
算法也要加上注释的,尤其那些深奥的算法。大家不可能都是算法专家,能一下子就通过代码理解到算法实现的真谛。所以,这里也要加上注释,一般是说明这是用了个什么算法,这套算法的出处或者附上相关文章的引用地址。
3. 非常规的写法
非常规的写法往往是有特殊情况,不得已为之的。比如,为了得到更好的性能;又比如,为了修复一个 bug,却不想对代码进行大改动。
总之,非常规的写法就是反模式、反套路的,有时候甚至会违反程序员的直觉。像这些做法,必须在注释中写明这样实现的原因。
4. 可能有坑却暂时没太好解决办法
有些时候,需求出的够难够复杂,时间上催的又很急,你根本没办法马上想到特别好的办法去实现。只能临时想个简单粗暴的方案,先凑活着。甚至还会在某些地方,把一些变量的值写死先去把本期的需求实现了。
像这种就很可能就会给后面挖坑了。这时候,注释必须加上为什么要这么解决的原因,还必须加上 //TODO 这类的,表示后面需要进行进一步的修改。
5. 关于项目核心的接口、类和字段
做项目的时候,需求中的很多核心概念很可能会被映射到对应的接口或者实体类上,如果在这些核心接口和实体类加上清楚的注释,写明对应的业务概念,那么,后面再维护项目的时候,真的是事半功倍。
比如,我们在一套批量调度系统里,可能有多种任务的概念,有需要限定执行时间的任务,也有不需要限定执行时间的任务,那么实现上,就可能有个 LimitedTimeTask 类对应限定时间的任务,还有个 UnLimitedTimeTask 类去对应不需要限定执行时间的任务。那这两个类就必须加上注解,写清楚对应的业务概念。
如果特定概念是复合的,是由多个小概念构成,却必须用一个接口或者一个类来表示,那很可能实现上,就还得用字段去映射这些小概念,那么这些字段也得加上注释说明起对应的概念。
总之,注释我个人理解必须要有,但是不可能太泛滥,必须有节制、有规范的加。
最后,咱们说白了,我对注释的态度就是,和写代码一样,要有规范。
在这里和管理者说一句,如果你希望大家写好注释,不能就靠一句“必须写注释”这么高高在上的话去要求大家。没有规范,你就不能完全怪程序员不加注释了。
最最后,大刘提醒我“四哥,注释规范里还要再加一条”
本文转载自微信公众号「四猿外」,可以通过以下二维码关注。转载本文请联系四猿外公众号。