代码整洁之道Ⅱ优雅注释之道

1,777次阅读

一、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

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

其它

待完善

微信扫描下方的二维码阅读本文

代码整洁之道Ⅱ优雅注释之道

 
Alan明宇
版权声明:本文于2023-06-21转载自本文转自阿里云开发社区 竹涧,共计14741字。
转载提示:此文章非本站原创文章,若需转载请联系原作者获得转载授权。