代碼整潔之道(二)優雅注釋之道

代碼整潔之道(二)優雅注釋之道

來自專欄我是程序員38 人贊了文章

一、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("cant 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("cant 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("cant 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)@Documentedpublic @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)@Documentedpublic @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。

本文作者:竹澗

原文鏈接

更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎


推薦閱讀:

計算器 日曆(代碼)
07年12月最近QQ空間免費代碼
webpack4.x多頁面配置
放置首頁左面的報刋書架及代碼
製作邊框代碼

TAG:代碼 | 計算機科學 |