代码整洁之道(二)优雅注释之道

一、Best Practice

注释应该声明代码的高层次意图,而非明显的细节

反例

    /**
     * generate signature by code, the algorithm is as follows:
     * 1.sort the http params, if you use java, you can easily use treeMap data structure
     * 2.join the param k-v
     * 3.use hmac-sha1 encrypt the specified string
     *
     * @param params request params
     * @param secret auth secret
     * @return secret sign
     * @throws Exception  exception
     */
    public static String generateSignature(Map<String, Object> params, String secret) throws Exception {
        final StringBuilder paramStr = new StringBuilder();
        final Map<String, Object> sortedMap = new TreeMap<>(params);
        for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
            paramStr.append(entry.getKey());
            paramStr.append(entry.getValue());
        }

        Mac hmac = Mac.getInstance("HmacSHA1");
        SecretKeySpec sec = new SecretKeySpec(secret.getBytes(), "HmacSHA1");
        hmac.init(sec);
        byte[] digest = hmac.doFinal(paramStr.toString().getBytes());

        return new String(new Hex().encode(digest), "UTF-8");
    }

说明

上文方法用于根据参数生成签名,注释中详细描述了签名算法的实现步骤,这其实就是过度描述代码明显细节

正例

   /**
     * generate signature by params and secret, used for computing signature for http request.
     *
     * @param params request params
     * @param secret auth secret
     * @return secret sign
     * @throws Exception  exception
     */
    public static String generateSignature(Map<String, Object> params, String secret) throws Exception {
        final StringBuilder paramStr = new StringBuilder();
        final Map<String, Object> sortedMap = new TreeMap<>(params);
        for (Map.Entry<String, Object> entry : sortedMap.entrySet()) {
            paramStr.append(entry.getKey());
            paramStr.append(entry.getValue());
        }

        Mac hmac = Mac.getInstance("HmacSHA1");
        SecretKeySpec sec = new SecretKeySpec(secret.getBytes(), "HmacSHA1");
        hmac.init(sec);
        byte[] digest = hmac.doFinal(paramStr.toString().getBytes());

        return new String(new Hex().encode(digest), "UTF-8");
    }

总结

  • 注释一定是表达代码之外的东西,代码可以包含的内容,注释中一定不要出现
  • 如果有必要注释,请注释意图(why),而不要去注释实现(how),大家都会看代码

在文件/类级别使用全局注释来解释所有部分如何工作

正例

/**
 * <p>
 * Helpers for {@code java.lang.System}.
 * </p>
 * <p>
 * If a system property cannot be read due to security restrictions, the corresponding field in this class will be set
 * to {@code null} and a message will be written to {@code System.err}.
 * </p>
 * <p>
 * #ThreadSafe#
 * </p>
 *
 * @since 1.0
 * @version $Id: SystemUtils.java 1583482 2014-03-31 22:54:57Z niallp $
 */
public class SystemUtils {}

总结

通常每个文件或类都应该有一个全局注释来概述该类的作用

公共api需要添加注释,其它代码谨慎使用注释

反例

/**
 *
 * @author yzq
 * @date 2017
 */
public interface KeyPairService {

    PlainResult<KeyPairInfoModel> createKeyPair(KeyPairCreateParam createParam);
}

说明

以上接口提供dubbo rpc服务属于公共api,以二方包的方式提供给调用方,虽然代码简单缺少了接口概要描述及方法注释等基本信息。

正例


/**
 * dubbo service: key pair rpc service api.
 *
 * @author yzq
 * @date 2017/02/22
 */
public interface KeyPairService {

    /**
     * create key pair info.
     *
     * @param createParam key pair create param
     * @return BaseResult
     */
    PlainResult<KeyPairInfoModel> createKeyPair(KeyPairCreateParam createParam);
}

总结

公共api一定要有注释,类文件使用类注释,公共接口方法用方法注释

在注释中用精心挑选的输入输出例子进行说明

正例

    /**
     * <p>Checks if CharSequence contains a search character, handling {@code null}.
     * This method uses {@link String#indexOf(int)} if possible.</p>
     *
     * <p>A {@code null} or empty ("") CharSequence will return {@code false}.</p>
     *
     * <pre>
     * StringUtils.contains(null, *)    = false
     * StringUtils.contains("", *)      = false
     * StringUtils.contains("abc", 'a') = true
     * StringUtils.contains("abc", 'z') = false
     * </pre>
     *
     * @param seq  the CharSequence to check, may be null
     * @param searchChar  the character to find
     * @return true if the CharSequence contains the search character,
     *  false if not or {@code null} string input
     * @since 2.0
     * @since 3.0 Changed signature from contains(String, int) to contains(CharSequence, int)
     */
    public static boolean contains(final CharSequence seq, final int searchChar) {
        if (isEmpty(seq)) {
            return false;
        }
        return CharSequenceUtils.indexOf(seq, searchChar, 0) >= 0;
    }

总结

对于公共的方法尤其是通用的工具类方法提供输入输出的例子往往比任何语言都有力

注释一定要描述离它最近的代码

反例

    private Map<String, String> buildInstanceDocumentMap(String version, String instanceId) {
        Map<String, String> instanceDocumentMap = Maps.newLinkedHashMap();

        Map<String, String> instanceDocumentMapMetadataPart = metaDataService.getInstanceDocument(instanceId, version,
            instanceDocumentMetaKeys);
        instanceDocumentMap.putAll(instanceDocumentMapMetadataPart);
        //the map must remove the old key for instance type
        instanceDocumentMap.put("instance-type", instanceDocumentMap.get("instance/instance-type"));
        instanceDocumentMap.remove("instance/instance-type");

        return instanceDocumentMap;
    }

说明

该方法有一行代码从map里删除了一个数据,注释放在了put调用之前,而没有直接放在remove之前

正例

    private Map<String, String> buildInstanceDocumentMap(String version, String instanceId) {
        Map<String, String> instanceDocumentMap = Maps.newLinkedHashMap();

        Map<String, String> instanceDocumentMapMetadataPart = metaDataService.getInstanceDocument(instanceId, version,
            instanceDocumentMetaKeys);
        instanceDocumentMap.putAll(instanceDocumentMapMetadataPart);
        instanceDocumentMap.put("instance-type", instanceDocumentMap.get("instance/instance-type"));
        //the map must remove the old key for instance type
        instanceDocumentMap.remove("instance/instance-type");

        return instanceDocumentMap;
    }

总结

注释要放在距离其描述代码最近的位置

注释一定要与代码对应

反例

    /**
     * 根据hash过后的id生成指定长度的随机字符串, 且长度不能超过16个字符
     * 
     * @param len length of string
     * @param  id id
     * @return String
     */
    public static String randomStringWithId(int len, long id) {
        if (len < 1 || len > 32) {
            throw new UnsupportedOperationException("can't support to generate 1-32 length random string");
        }
        //use default random seed
        StringBuffer sb = new StringBuffer();
        long genid = id;
        for (int i = 0; i < len; i++) {
            long pos = genid%32 ;
            genid = genid>>6;
            sb.append(RANDOM_CHAR[(int) pos]);
        }
        return sb.toString();

    }

说明

注释中说明生成随机字符串的长度不能超过16字符,实际代码已经修改为32个字符,此处注释会产生误导读者的副作用

正例

    /**
     * 根据hash过后的id生成指定长度的随机字符串
     * 
     * @param len length of string
     * @param  id id
     * @return String
     */
    public static String randomStringWithId(int len, long id) {
        if (len < 1 || len > 32) {
            throw new UnsupportedOperationException("can't support to generate 1-32 length random string");
        }
        //use default random seed
        StringBuffer sb = new StringBuffer();
        long genid = id;
        for (int i = 0; i < len; i++) {
            long pos = genid%32 ;
            genid = genid>>6;
            sb.append(RANDOM_CHAR[(int) pos]);
        }
        return sb.toString();

    }

总结

  • 注释一定要与代码对应,通常代码变化对应的注释也要随之改变
  • 若非必要慎用注释,注释同代码一样需要维护更新

一定要给常量加注释

反例

/**
 * define common constants for ebs common component.
 *
 * Author: yzq Date: 16/7/12 Time: 17:44
 */
public final class CommonConstants {

    /**
     * keep singleton
     */
    private CommonConstants() {}

    public static final String BILLING_BID = "26842";

    public static final int BILLING_DOMAIN_INTEGRITY_VALID = 1;

    public static final int BILLING_READYFLAG_START = 0;
}

正例

/**
 * define common constants for ebs common component.
 *
 * Author: yzq Date: 16/7/12 Time: 17:44
 */
public final class CommonConstants {

    /**
     * keep singleton
     */
    private CommonConstants() {}


    /**
     * oms client bid.
     */
    public static final String BILLING_BID = "26842";

    /**
     * oms billing domain integrity true.
     */
    public static final int BILLING_DOMAIN_INTEGRITY_VALID = 1;

    /**
     * oms billing readyflag start.
     */
    public static final int BILLING_READYFLAG_START = 0;
}

总结

  • 给每一个常量加一个有效的注释

巧用标记(TODO,FIXME,HACK)

  • TODO 有未完成的事项
  • FIXME 代码有已知问题待修复
  • HACK 表示代码有hack逻辑

示例

    public static String randomStringWithId(int len, long id) {
        // TODO: 2018/6/11 需要将len的合法范围抽象
        if (len < 1 || len > 32) {
            throw new UnsupportedOperationException("can't support to generate 1-32 length random string");
        }
        //use default random seed
        StringBuffer sb = new StringBuffer();
        long genid = id;
        for (int i = 0; i < len; i++) {
            long pos = genid%32 ;
            genid = genid>>6;
            sb.append(RANDOM_CHAR[(int) pos]);
        }
        return sb.toString();

    }

配置标记

可以扩展IDE修改标记的配置,比如加入解决人,关联缺陷等信息,以IDEA为例修改入口如下:

代码整洁之道(二)优雅注释之道

总结

  • 巧用TODO、FIXME、HACK等注解标识代码
  • 及时处理所有标识代码,忌滥用

适当添加警示注释

正例


    private BaseResult putReadyFlag(BillingDataContext context, Integer readyFlag) {
        // warn! oms data format require List<Map<String,String>> and the size of it must be one.
        List<Map<String, String>> dataList = Lists.newArrayListWithExpectedSize(1);
    }

说明

该方法创建了一个大小固定为1且类型为Map<String,String>的数组链表,这个用法比较奇怪,需要注释说明原因

总结

代码里偶尔出现一些非常hack的逻辑且修改会引起较高风险,这个时候需要加注释重点说明

注释掉的代码

反例

    private Object buildParamMap(Object request) throws Exception {
        if (List.class.isAssignableFrom(request.getClass())) {
            List<Object> input = (List<Object>)request;
            List<Object> result = new ArrayList<Object>();
            for (Object obj : input) {
                result.add(buildParamMap(obj));
            }
            return result;
        }

        Map<String, Object> result = new LinkedHashMap<String, Object>();
        Field[] fields = FieldUtils.getAllFields(request.getClass());
        for (Field field : fields) {
            if (IGNORE_FIELD_LIST.contains(field.getName())) {
                continue;
            }

            String fieldAnnotationName = field.getAnnotation(ProxyParam.class) != null ? field.getAnnotation(
                ProxyParam.class).paramName() : HttpParamUtil.convertParamName(field.getName());

            //Object paramValue = FieldUtils.readField(field, request, true);
            //if (paramValue == null) {
            //    continue;
            //}
            //
            //if (BASIC_TYPE_LIST.contains(field.getGenericType().getTypeName())) {
            //    result.put(fieldAnnotationName, String.valueOf(paramValue));
            //} else {
            //    result.put(fieldAnnotationName, this.buildParamMap(paramValue));
            //}

        }
        return result;
    }

说明

常见套路,为了方便需要的时候重新复用废弃代码,直接注释掉。

正例

同上,删除注释部分代码

总结

不要在代码保留任何注释掉的代码,版本管理软件如Git可以做的事情不要放到代码里

循规蹈矩式注释

反例

/**
 * 类EcsOperateLogDO.java的实现描述:TODO 类实现描述
 * 
 * @author xxx 2012-12-6 上午10:53:21
 */
public class DemoDO implements Serializable {

    private static final long serialVersionUID = -3517141301031994021L;

    /**
     * 主键id
     */
    private Long              id;

    /**
     * 用户uid
     */
    private Long              aliUid;

    /**
     * @return the id
     */
    public Long getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * @return the aliUid
     */
    public Long getAliUid() {
        return aliUid;
    }

    /**
     * @param aliUid the aliUid to set
     */
    public void setAliUid(Long aliUid) {
        this.aliUid = aliUid;
    }
}

说明

分析上述代码可以发现两处注释非常别扭和多余:

  • 类注释使用了默认模版, 填充了无效信息
  • IDE为Getter及Setter方法生成了大量的无效注释

正例

/**
 * Demo model.
 * @author xxx 2012-12-6 上午10:53:21
 */
public class DemoDO implements Serializable {

    private static final long serialVersionUID = -3517141301031994021L;

    /**
     * 主键id
     */
    private Long              id;

    /**
     * 用户uid
     */
    private Long              aliUid;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getAliUid() {
        return aliUid;
    }

    public void setAliUid(Long aliUid) {
        this.aliUid = aliUid;
    }
}

总结

  • 不要保留任何循规蹈矩式注释,比如IDE自动生成的冗余注释
  • 不要产生任何该类注释,可以统一配置IDE达到该效果,推荐使用灵狐插件

日志式注释

反例

     /** 支持xxx   code by xxx 2015/10/11  */
        String countryCode = param.getCountyCode();
        if(StringUtils.isNotBlank(countryCode) && !"CN".equals(countryCode)){
            imageOrderParam.setCountyCode(param.getCountyCode());
            imageOrderParam.setCurrency(param.getCurrency());
        }

说明

修改已有代码很多人会手动添加注释说明修改日期,修改人及修改说明等信息,这些信息大多是冗余的

正例

代码同上,删除该注释

总结

不要在代码中加入代码的著作信息,版本管理可以完成的事情不要做在代码里

“拐杖注释”

反例

   /**
     * update config map, if the config map is not exist, create it then put the specified key and value, then return it
     * @param key config key
     * @param value config value
     * @return config map
     */
    public Map<String, String> updateConfigWithSpecifiedKV(final String key, final String value) {
        if (StringUtils.isNotBlank(key) || StringUtils.isNotBlank(value)) {
            return Maps.newHashMap();
        }
        
        Map<String, String> config = queryConfigMap();
        if (MapUtils.isEmpty(config)) {
            return new HashMap<String, String>() {{
                put(key, value);
            }};
        }

        config.put(key, value);
        return config;
    }

说明

示例代码简单实现了更新指定map k-v等功能,如果目标map不存在则使用指定k-v初始化一个map并返回,方法名为 updateConfigWithSpecifiedKV ,为了说明方法的完整意图,注释描述了方法的实现逻辑

正例

    /**
     * create or update config map with specified k-v.
     *
     * @param value config value
     * @return config map
     */
    public Map<String, String> createOrUpdateConfigWithSpecifiedKV(final String key, final String value) {
        if (StringUtils.isNotBlank(key) || StringUtils.isNotBlank(value)) {
            return Maps.newHashMap();
        }

        Map<String, String> config = queryConfigMap();
        if (MapUtils.isEmpty(config)) {
            return new HashMap<String, String>() {{
                put(key, value);
            }};
        }

        config.put(key, value);
        return config;
    }

总结

抛弃“拐杖注释”,不要给不好的名字加注释,一个好的名字比好的注释更重要

过度html化的注释

反例

/**
 * used for indicate the field will be used as a http param, the http request methods include as follows:
 * <li>Get</li>
 * <li>Post</li>
 * <li>Connect</li>
 *
 * the proxy param will be parsed, see {@link ProxyParamBuilder}.
 *
 * @author yzq
 * @date 2017/12/08
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ProxyParam {

    /**
     * the value indicate the proxy app name, such as houyi.
     *
     * @return proxy app name
     */
    String proxyApp() default "houyi";

    /**
     * proxy request mapping http param.
     *
     * @return http param
     */
    String paramName();

    /**
     * the value indicate if the param is required.
     *
     * @return if this param is required
     */
    boolean isRequired() default true;
}

说明

类注释使用了大量的html标签用来描述,实际效果并没有带来收益反而增加阅读难度

正例

/**
 * used for indicate the field will be used as a http param.
 *
 * @author yzq
 * @date 2017/12/08
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ProxyParam {

    /**
     * the value indicate the proxy app name, such as houyi.
     *
     * @return proxy app name
     */
    String proxyApp() default "houyi";

    /**
     * proxy request mapping http param.
     *
     * @return http param
     */
    String paramName();

    /**
     * the value indicate if the param is required.
     *
     * @return if this param is required
     */
    boolean isRequired() default true;
}

总结

  • 普通业务注释谨慎使用html标签,它不会给你带来明显收益,只会徒增阅读难度
  • 如果是公共api且用于生成javadoc可以考虑加入必要的html标签,比如链接,锚点等

二、编程语言注释实践

Java

文件/类注释规范

目前IDE安装 灵狐 后会自动配置IDE的file templates为如下格式:

/**
 * @author ${USER}
 * @date ${YEAR}/${MONTH}/${DAY}
 */

__强烈建议使用如上配置,统一、简洁就是最好。__如果有特殊需要需要定制类注释可以参考下图:

代码整洁之道(二)优雅注释之道

方法注释

    /**  
     * xxx
     * 
     * @param 
     * @param 
     * @return 
     * @throws 
     */

IDE提供了统一的方法注释模版,无需手动配置,好的方法注释应该包括以下内容:

  • 方法的描述,重点描述该方法用来做什么,有必要可以加一个输入输出的例子
  • 参数描述
  • 返回值描述
  • 异常描述

举个例子:

    /**
     * Converts a <code>byte[]</code> to a String using the specified character encoding.
     *
     * @param bytes
     *            the byte array to read from
     * @param charsetName
     *            the encoding to use, if null then use the platform default
     * @return a new String
     * @throws UnsupportedEncodingException
     *             If the named charset is not supported
     * @throws NullPointerException
     *             if the input is null
     * @deprecated use {@link StringUtils#toEncodedString(byte[], Charset)} instead of String constants in your code
     * @since 3.1
     */
    @Deprecated
    public static String toString(final byte[] bytes, final String charsetName) throws UnsupportedEncodingException {
        return charsetName != null ? new String(bytes, charsetName) : new String(bytes, Charset.defaultCharset());
    }

块注释与行注释

  • 单行代码注释使用行注释 //
  • 多行代码注释使用块注释 /* */

Python

文件注释

  • 重点描述文件的作用及使用方式
#!/usr/bin/python
# -*- coding: UTF-8 -*-

"""
bazaar script collection.

init_resource_entry, used for init bazaar resource such as vpc, vsw, sg, proxy ecs and so on.

user manual:
1. modify ecs.conf config your key, secret, and region.
2. run bazaar_tools.py script, this process will last a few minutes,then it will generate a init.sql file.
3. use idb4 submit your ddl changes.

"""

类注释

    """
    ecs sdk client, used for xxx.

    Attributes:
        client:
        access_key: 
        access_secret:
        region:
    """
  • 类应该在其定义下有一个用于描述该类的文档字符串
  • 类公共属性应该加以描述

函数注释

def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
    """Fetches rows from a Bigtable.

    Retrieves rows pertaining to the given keys from the Table instance
    represented by big_table.  Silly things may happen if
    other_silly_variable is not None.

    Args:
        big_table: An open Bigtable Table instance.
        keys: A sequence of strings representing the key of each table row
            to fetch.
        other_silly_variable: Another optional variable, that has a much
            longer name than the other args, and which does nothing.

    Returns:
        A dict mapping keys to the corresponding table row data
        fetched. Each row is represented as a tuple of strings. For
        example:

        {'Serak': ('Rigel VII', 'Preparer'),
         'Zim': ('Irk', 'Invader'),
         'Lrrr': ('Omicron Persei 8', 'Emperor')}

        If a key from the keys argument is missing from the dictionary,
        then that row was not found in the table.

    Raises:
        IOError: An error occurred accessing the bigtable.Table object.
    """
    pass
  • Args:列出每个参数的名字, 并在名字后使用一个冒号和一个空格, 分隔对该参数的描述.如果描述太长超过了单行80字符,使用2或者4个空格的悬挂缩进(与文件其他部分保持一致). 描述应该包括所需的类型和含义. 如果一个函数接受*foo(可变长度参数列表)或者**bar (任意关键字参数), 应该详细列出*foo和**bar.
  • Returns: 描述返回值的类型和语义. 如果函数返回None, 这一部分可以省略
  • Raises:列出与接口有关的所有异常

多行注释与行尾注释

# We use a weighted dictionary search to find out where i is in
# the array.  We extrapolate position based on the largest num
# in the array and the array size and then do binary search to
# get the exact number.

if i & (i-1) == 0:        # true iff i is a power of 2
  • 复杂操作多行注释描述
  • 比较晦涩的代码使用行尾注释

Golang

行注释

常用注释风格

包注释

/**/ 通常用于包注释, 作为一个整体提供此包的对应信息,每个包都应该包含一个doc.go用于描述其信息。

/*
     ecs OpenApi demo,use aliyun ecs sdk manage ecs, this package will provide you function list as follows:

    DescribeInstances, query your account ecs.
    CreateInstance, create a ecs vm with specified params.
 */
package ecsproxy

JavaScript

常用/**/与//,用法基本同Java。

Shell

只支持 # ,每个文件都包含一个顶层注释,用于阐述版权及概要信息。

其它

待完善

小结

本文先总结了注释在编程中的最佳实践场景并举例进行了说明,然后就不同编程语言提供了一些注释模版及规范相关的实践tips。关于注释我个人的认知是:注释即代码,注释即文档,写好注释一个工程师必备的素质之一,在整洁代码前提下,更少的注释是跟高的追求。关于注释的实践暂时写到这里,后面会持续完善,也欢迎大家提供好的tips,文中代码大多出自于日常业务项目,也有部分摘自开源库,若有不妥之处敬请指正。

上一篇:快速管理阿里云资源与*钱关系——资源标记(TAG)策略


下一篇:MaxCompute单字段拆分多行多列