程式碼整潔之道(二)優雅註釋之道
一、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
final StringBuilder paramStr = new StringBuilder();
final Map
for (Map。Entry
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
final StringBuilder paramStr = new StringBuilder();
final Map
for (Map。Entry
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),大家都會看程式碼
在檔案/類級別使用全域性註釋來解釋所有部分如何工作
正例
/**
*
* Helpers for {@code java。lang。System}。
*
*
* 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}。
*
*
* #ThreadSafe#
*
*
* @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
}
說明
以上介面提供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
}
總結
公共api一定要有註釋,類檔案使用類註釋,公共介面方法用方法註釋
在註釋中用精心挑選的輸入輸出例子進行說明
正例
/**
*
Checks if CharSequence contains a search character, handling {@code null}。
* This method uses {@link String#indexOf(int)} if possible。
*
*
A {@code null} or empty (“”) CharSequence will return {@code false}。
*
*
* StringUtils。contains(null, *) = false
* StringUtils。contains(“”, *) = false
* StringUtils。contains(“abc”, ‘a’) = true
* StringUtils。contains(“abc”, ‘z’) = false
*
*
* @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
Map
Map
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
Map
Map
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
}
說明
該方法建立了一個大小固定為1且型別為Map
總結
程式碼裡偶爾出現一些非常hack的邏輯且修改會引起較高風險,這個時候需要加註釋重點說明
註釋掉的程式碼
反例
private Object buildParamMap(Object request) throws Exception {
if (List。class。isAssignableFrom(request。getClass())) {
List
List
for (Object obj : input) {
result。add(buildParamMap(obj));
}
return result;
}
Map
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
if (StringUtils。isNotBlank(key) || StringUtils。isNotBlank(value)) {
return Maps。newHashMap();
}
Map
if (MapUtils。isEmpty(config)) {
return new HashMap
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
if (StringUtils。isNotBlank(key) || StringUtils。isNotBlank(value)) {
return Maps。newHashMap();
}
Map
if (MapUtils。isEmpty(config)) {
return new HashMap
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:
*
*
*
*
* 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)@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 byte[]
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。
本文作者:竹澗
原文連結
更多技術乾貨敬請關注雲棲社群知乎機構號:阿里云云棲社群 - 知乎