整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

在JavaScript中编写更好的条件的5个技巧

在JavaScript中编写更好的条件的5个技巧

.对多个条件使用Array.includes

我们来看看下面的例子:

// condition
function test(fruit) {
 if (fruit=='apple' || fruit=='strawberry') {
 console.log('red');
 }
}

乍一看,上面的例子看起来不错。然而,如果我们得到更多的红色水果,说的cherry和cranberries?我们是否会更多地扩展声明||?

我们可以使用(Array.includes)重写上面的条件Array.includes

function test(fruit) {
 // extract conditions to array
 const redFruits=['apple', 'strawberry', 'cherry', 'cranberries'];
 if (redFruits.includes(fruit)) {
 console.log('red');
 }
}

我们将red fruits(条件)提取到数组中。通过这样做,代码看起来更整洁。


2.较少嵌套,早退

让我们扩展前面的示例以包含另外两个条件:

  • 如果没有提供水果,抛出错误
  • 如果超过10,则接受并打印水果数量。
function test(fruit, quantity) {
 const redFruits=['apple', 'strawberry', 'cherry', 'cranberries'];
 // condition 1: fruit must has value
 if (fruit) {
 // condition 2: must be red
 if (redFruits.includes(fruit)) {
 console.log('red');
 // condition 3: must be big quantity
 if (quantity > 10) {
 console.log('big quantity');
 }
 }
 } else {
 throw new Error('No fruit!');
 }
}
// test results
test(null); // error: No fruits
test('apple'); // print: red
test('apple', 20); // print: red, big quantity

看看上面的代码,我们有:

  • 1 if / else语句过滤掉无效条件
  • 3级嵌套if语句(条件1,2和3)

我个人遵循的一般规则是发现无效条件时提前返回

/_ return early when invalid conditions found _/
function test(fruit, quantity) {
 const redFruits=['apple', 'strawberry', 'cherry', 'cranberries'];
 // condition 1: throw error early
 if (!fruit) throw new Error('No fruit!');
 // condition 2: must be red
 if (redFruits.includes(fruit)) {
 console.log('red');
 // condition 3: must be big quantity
 if (quantity > 10) {
 console.log('big quantity');
 }
 }
}

通过这样做,我们有一个较少级别的嵌套语句。这种编码风格很好,特别是当你有很长的if语句时(想象你需要滚动到最底层才知道有一个else语句,而不是很酷)。

如果通过反转条件并提前返回,我们可以进一步减少嵌套。请查看下面的条件2,看看我们是如何做到的:


/_ return early when invalid conditions found _/
function test(fruit, quantity) {
 const redFruits=['apple', 'strawberry', 'cherry', 'cranberries'];
 if (!fruit) throw new Error('No fruit!'); // condition 1: throw error early
 if (!redFruits.includes(fruit)) return; // condition 2: stop when fruit is not red
 console.log('red');
 // condition 3: must be big quantity
 if (quantity > 10) {
 console.log('big quantity');
 }
}

通过反转条件2的条件,我们的代码现在没有嵌套语句。当我们有很长的逻辑时,这种技术非常有用,我们希望在条件不满足时停止进一步的处理。

但是,这样做并不是一件难事。问问自己,这个版本(没有嵌套)比前一个更好/更可读(条件2嵌套)?

对我来说,我只是把它留作以前的版本(条件2嵌套)。这是因为:

  • 代码简短直接,嵌套if更清晰
  • 反转条件可能会引发更多的思考过程(增加认知负荷)

因此,始终旨在尽早减少筑巢和回归,但不要过度。如果您感兴趣,有一篇文章和StackOverflow讨论会进一步讨论这个主题:

  • 避免其他,早期由蒂姆奥克斯利回归
  • StackOverflow讨论 if / else编码风格

3.使用默认功能参数和解构

我想下面的代码可能看起来很熟悉,我们总是需要检查null/ undefined值并在使用JavaScript时分配默认值:

function test(fruit, quantity) {
 if (!fruit) return;
 const q=quantity || 1; // if quantity not provided, default to one
 console.log(`We have ${q} ${fruit}!`);
}
//test results
test('banana'); // We have 1 banana!
test('apple', 2); // We have 2 apple!

实际上,我们可以q通过分配默认函数参数来消除变量。

function test(fruit, quantity=1) { // if quantity not provided, default to one
 if (!fruit) return;
 console.log(`We have ${quantity} ${fruit}!`);
}
//test results
test('banana'); // We have 1 banana!
test('apple', 2); // We have 2 apple!

更简单直观不是吗?请注意,每个参数都有自己的默认函数参数。例如,我们也可以指定默认值fruit:function test(fruit='unknown', quantity=1)。

如果我们fruit是一个对象怎么办?我们可以指定默认参数吗?

function test(fruit) { 
 // printing fruit name if value provided
 if (fruit && fruit.name) {
 console.log (fruit.name);
 } else {
 console.log('unknown');
 }
}
//test results
test(undefined); // unknown
test({ }); // unknown
test({ name: 'apple', color: 'red' }); // apple

看看上面的例子,我们想要打印水果名称,如果它可用,或者我们将打印未知。我们可以避免fruit && fruit.name使用默认函数参数和破坏进行条件检查。

// destructing - get name property only
// assign default empty object {}
function test({name}={}) {
 console.log (name || 'unknown');
}
//test results
test(undefined); // unknown
test({ }); // unknown
test({ name: 'apple', color: 'red' }); // apple

由于我们只需要name来自水果的属性,我们可以使用构造参数{name},然后我们可以name在代码中使用变量代替fruit.name。

我们还将空对象指定{}为默认值。如果我们不这样做,你在执行行时会出错test(undefined)- Cannot destructure property name of 'undefined' or 'null'.因为nameundefined中没有属性。

如果您不介意使用第三方库,有几种方法可以减少空检查:

  • 使用Lodash获取功能
  • 使用Facebook开源的idx库(与Babeljs)

以下是使用Lodash的示例:

// Include lodash library, you will get _
function test(fruit) {
 console.log(__.get(fruit, 'name', 'unknown'); // get property name, if not available, assign default value 'unknown'
}
//test results
test(undefined); // unknown
test({ }); // unknown
test({ name: 'apple', color: 'red' }); // apple

您可以在此处运行演示代码。此外,如果您是功能编程(FP)的粉丝,您可以选择使用Lodash fp,Lodash的功能版本(方法更改为get或getOr)。

4.支持Map / Object Literal而不是Switch语句

让我们看看下面的例子,我们想根据颜色打印水果:

function test(color) {
 // use switch case to find fruits in color
 switch (color) {
 case 'red':
 return ['apple', 'strawberry'];
 case 'yellow':
 return ['banana', 'pineapple'];
 case 'purple':
 return ['grape', 'plum'];
 default:
 return [];
 }
}
//test results
test(null); // []
test('yellow'); // ['banana', 'pineapple']

上面的代码似乎没有错,但我觉得它很冗长。使用具有更清晰语法的object literal可以实现相同的结果:

// use object literal to find fruits in color
 const fruitColor={
 red: ['apple', 'strawberry'],
 yellow: ['banana', 'pineapple'],
 purple: ['grape', 'plum']
 };
function test(color) {
 return fruitColor[color] || [];
}

或者,您可以使用Map来实现相同的结果:

// use Map to find fruits in color
 const fruitColor=new Map()
 .set('red', ['apple', 'strawberry'])
 .set('yellow', ['banana', 'pineapple'])
 .set('purple', ['grape', 'plum']);
function test(color) {
 return fruitColor.get(color) || [];
}

Map是自ES2015以来可用的对象类型,允许您存储键值对。

我们应该禁止使用switch语句吗?不要局限于此。就个人而言,我尽可能使用对象文字,但我不会设置硬规则来阻止它,使用对你的场景有意义的。

Todd Motto有一篇文章深入研究switch语句与对象文字,你可以在这里阅读。

TL; DR; 重构语法

对于上面的例子,我们实际上可以重构我们的代码以获得相同的结果Array.filter。

 const fruits=[
 { name: 'apple', color: 'red' }, 
 { name: 'strawberry', color: 'red' }, 
 { name: 'banana', color: 'yellow' }, 
 { name: 'pineapple', color: 'yellow' }, 
 { name: 'grape', color: 'purple' }, 
 { name: 'plum', color: 'purple' }
];
function test(color) {
 // use Array filter to find fruits in color
 return fruits.filter(f=> f.color==color);
}

总有不止一种方法可以达到相同的效果。我们用相同的例子展示了4。编码很有趣!

5.对所有/部分标准使用Array.every和Array.some

、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,文中代码大多出自于日常业务项目,也有部分摘自开源库,若有不妥之处敬请指正。

任何编程语言中,代码需要根据不同的条件在给定的输入中做不同的决定和执行相应的动作。

例如,在一个游戏中,如果玩家生命点为0,游戏结束。在天气应用中,如果在早上被查看,显示一个日出图片,如果是晚上,则显示星星和月亮。在这篇文章中,我们将探索JavaScript中所谓的条件语句如何工作。

如果你使用JavaScript工作,你将写很多包含条件调用的代码。条件调用可能初学很简单,但是还有比写一对对if/else更多的东西。这里有些编写更好更清晰的条件代码的有用提示。

  1. 数组方法 Array.includes
  2. 提前退出 / 提前返回
  3. 用对象字面量或Map替代Switch语句
  4. 默认参数和解构
  5. 用 Array.every & Array.some 匹配全部/部分内容
  6. 使用可选链和空值合并

1. 数组方法 Array.includes

使用 Array.includes 进行多条件选择

例如:

   function printAnimals(animal) {
   if (animal==='dog' || animal==='cat') {
      console.log(I have a ${animal});
    }
   }

   console.log(printAnimals('dog')); // I have a dog


上面的代码看起来很好因为我们只检查了两个动物。然而,我们不确定用户输入。如果我们要检查任何其他动物呢?如果我们通过添加更多“或”语句来扩展,代码将变得难以维护和不清晰。

解决方案:

我们可以通过使用 Array.includes 来重写上面的条件

  function printAnimals(animal) {
   const animals=['dog', 'cat', 'hamster', 'turtle']; 

   if (animals.includes(animal)) {
     console.log(I have a ${animal});
   }
  }

  console.log(printAnimals('hamster')); // I have a hamster


这里,我们创建来一个动物数组,所以条件语句可以和代码的其余部分抽象分离出来。现在,如果我们想要检查任何其他动物,我们只需要添加一个新的数组项。

我们也能在这个函数作用域外部使用这个动物数组变量来在代码中的其他任意地方重用它。这是一个编写更清晰、易理解和维护的代码的方法,不是吗?

2. 提前退出 / 提前返回

这是一个精简你的代码的非常酷的技巧。我记得当我开始专业工作时,我在第一天学习使用提前退出来编写条件。

让我们在之前的例子上添加更多的条件。用包含确定属性的对象替代简单字符串的动物。

现在的需求是:

  • 如果没有动物,抛出一个异常
  • 打印动物类型
  • 打印动物名字
  • 打印动物性别
const printAnimalDetails=animal=> {
  let result; // declare a variable to store the final value

  // condition 1: check if animal has a value
  if (animal) {

    // condition 2: check if animal has a type property
    if (animal.type) {

      // condition 3: check if animal has a name property
      if (animal.name) {

        // condition 4: check if animal has a gender property
        if (animal.gender) {
          result=${animal.name} is a ${animal.gender} ${animal.type};;
        } else {
          result="No animal gender";
        }
      } else {
        result="No animal name";
      }
    } else {
      result="No animal type";
    }
  } else {
    result="No animal";
  }

  return result;
};

console.log(printAnimalDetails()); // 'No animal'

console.log(printAnimalDetails({ type: "dog", gender: "female" })); // 'No animal name'

console.log(printAnimalDetails({ type: "dog", name: "Lucy" })); // 'No animal gender'

console.log(
  printAnimalDetails({ type: "dog", name: "Lucy", gender: "female" })
); // 'Lucy is a female dog'



你觉得上面的代码怎么样?

它工作得很好,但是代码很长并且维护困难。如果不使用lint工具,找出闭合花括号在哪都会浪费很多时间。 想象如果代码有更复杂的逻辑会怎么样?大量的if..else语句。

我们能用三元运算符、&&条件等语法重构上面的功能,但让我们用多个返回语句编写更清晰的代码。

const printAnimalDetails=({type, name, gender }={})=> {
  if(!type) return 'No animal type';
  if(!name) return 'No animal name';
  if(!gender) return 'No animal gender';

// Now in this line of code, we're sure that we have an animal with all //the three properties here.

  return ${name} is a ${gender} ${type};
}

console.log(printAnimalDetails()); // 'No animal type'

console.log(printAnimalDetails({ type: dog })); // 'No animal name'

console.log(printAnimalDetails({ type: dog, gender: female })); // 'No animal name'

console.log(printAnimalDetails({ type: dog, name: 'Lucy', gender: 'female' })); // 'Lucy is a female dog'


在这个重构过的版本中,也包含了解构和默认参数。默认参数确保如果我们传递undefined作为一个方法的参数,我们仍然有值可以解构,在这里它是一个空对象{}。

通常,在专业领域,代码被写在这两种方法之间。

另一个例子:

  function printVegetablesWithQuantity(vegetable, quantity) {
  const vegetables=['potato', 'cabbage', 'cauliflower', 'asparagus'];

  // condition 1: vegetable should be present
   if (vegetable) {
     // condition 2: must be one of the item from the list
     if (vegetables.includes(vegetable)) {
       console.log(I like ${vegetable});

       // condition 3: must be large quantity
       if (quantity >=10) {
         console.log('I have bought a large quantity');
       }
     }
   } else {
     throw new Error('No vegetable from the list!');
   }
 }

 printVegetablesWithQuantity(null); //  No vegetable from the list!
 printVegetablesWithQuantity('cabbage'); // I like cabbage
 printVegetablesWithQuantity('cabbage', 20); 
 // 'I like cabbage
 // 'I have bought a large quantity'


现在,我们有:

  • 1 if/else 语句过滤非法条件
  • 3 级嵌套if语句 (条件 1, 2, & 3)

一个普遍遵循的规则是:在非法条件匹配时提前退出。

  function printVegetablesWithQuantity(vegetable, quantity) {

  const vegetables=['potato', 'cabbage', 'cauliflower', 'asparagus'];

   // condition 1: throw error early
   if (!vegetable) throw new Error('No vegetable from the list!');

   // condition 2: must be in the list
   if (vegetables.includes(vegetable)) {
      console.log(I like ${vegetable});

     // condition 3: must be a large quantity
      if (quantity >=10) {
        console.log('I have bought a large quantity');
      }
   }
 }


通过这么做,我们少了一个嵌套层级。当你有一个长的if语句时,这种代码风格特别好。

我们能通过条件倒置和提前返回,进一步减少嵌套的if语句。查看下面的条件2,观察我们是怎么做的:

  function printVegetablesWithQuantity(vegetable, quantity) {

  const vegetables=['potato', 'cabbage', 'cauliflower', 'asparagus'];

   if (!vegetable) throw new Error('No vegetable from the list!'); 
   // condition 1: throw error early

   if (!vegetables.includes(vegetable)) return; 
   // condition 2: return from the function is the vegetable is not in 
  //  the list 


  console.log(I like ${vegetable});

  // condition 3: must be a large quantity
  if (quantity >=10) {
      console.log('I have bought a large quantity');
  }
 }


通过倒置条件2,代码没有嵌套语句了。这种技术在我们有很多条件并且当任何特定条件不匹配时,我们想停止进一步处理的时候特别有用。

所以,总是关注更少的嵌套和提前返回,但也不要过度地使用。

3. 用对象字面量或Map替代Switch语句

让我们来看看下面的例子,我们想要基于颜色打印水果:

function printFruits(color) {
  // use switch case to find fruits by color
  switch (color) {
    case 'red':
      return ['apple', 'strawberry'];
    case 'yellow':
      return ['banana', 'pineapple'];
    case 'purple':
      return ['grape', 'plum'];
    default:
      return [];
  }
}

printFruits(null); // []
printFruits('yellow'); // ['banana', 'pineapple']


上面的代码没有错误,但是它仍然有些冗长。相同的功能能用对象字面量以更清晰的语法实现:

// use object literal to find fruits by color
  const fruitColor={
    red: ['apple', 'strawberry'],
    yellow: ['banana', 'pineapple'],
    purple: ['grape', 'plum']
  };

function printFruits(color) {
  return fruitColor[color] || [];
}


另外,你也能用Map来实现相同的功能:

// use Map to find fruits by color
  const fruitColor=new Map()
    .set('red', ['apple', 'strawberry'])
    .set('yellow', ['banana', 'pineapple'])
    .set('purple', ['grape', 'plum']);

function printFruits(color) {
  return fruitColor.get(color) || [];
}


Map 允许保存键值对,是自从ES2015以来可以使用的对象类型。

对于上面的例子,相同的功能也能用数组方法 Array.filte 来实现。

 const fruits=[
    { name: 'apple', color: 'red' }, 
    { name: 'strawberry', color: 'red' }, 
    { name: 'banana', color: 'yellow' }, 
    { name: 'pineapple', color: 'yellow' }, 
    { name: 'grape', color: 'purple' }, 
    { name: 'plum', color: 'purple' }
];

function printFruits(color) {
  return fruits.filter(fruit=> fruit.color===color);
}


4. 默认参数和解构

当使用 JavaScript 工作时,我们总是需要检查 null/undefined 值并赋默认值,否则可能编译失败。

  function printVegetablesWithQuantity(vegetable, quantity=1) { 
// if quantity has no value, assign 1

  if (!vegetable) return;
    console.log(We have ${quantity} ${vegetable}!);
  }

  //results
  printVegetablesWithQuantity('cabbage'); // We have 1 cabbage!
  printVegetablesWithQuantity('potato', 2); // We have 2 potato!


如果 vegetable 是一个对象呢?我们能赋一个默认参数吗?

  function printVegetableName(vegetable) { 
    if (vegetable && vegetable.name) {
     console.log (vegetable.name);
   } else {
    console.log('unknown');
   }
 }

 printVegetableName(undefined); // unknown
 printVegetableName({}); // unknown
 printVegetableName({ name: 'cabbage', quantity: 2 }); // cabbage


在上面的例子中,如果vegetable 存在,我们想要打印 vegetable name, 否则打印"unknown"。

我们能通过使用默认参数和解构来避免条件语句 if (vegetable && vegetable.name) {} 。

  // destructing - get name property only
  // assign default empty object {}

  function printVegetableName({name}={}) {
   console.log (name || 'unknown');
 }


 printVegetableName(undefined); // unknown
 printVegetableName({ }); // unknown
 printVegetableName({ name: 'cabbage', quantity: 2 }); // cabbage


因为我们只需要 name 属性,所以我们可以使用 { name } 解构参数,然后我们就能在代码中使用 name 作为变量,而不是 vegetable.name。

我们还赋了一个空对象 {} 作为默认值,因为当执行 printVegetableName(undefined) 时会得到一个错误:不能从 undefined 或 null 解构属性 name,因为在 undefined 中没有name属性。

5. 用 Array.every & Array.some 匹配全部/部分内容

我们能使用数组方法减少代码行。查看下面的代码,我们想要检查是否所有的水果都是红色的:

const fruits=[
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
  ];

function test() {
  let isAllRed=true;

  // condition: all fruits must be red
  for (let f of fruits) {
    if (!isAllRed) break;
    isAllRed=(f.color=='red');
  }

  console.log(isAllRed); // false
}


这代码太长了!我们能用 Array.every 来减少代码行数:

const fruits=[
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
  ];

function test() {
  // condition: short way, all fruits must be red
  const isAllRed=fruits.every(f=> f.color=='red');

  console.log(isAllRed); // false
}


相似地,如果我们想测试是否有任何红色的水果,我们能用一行 Array.some 来实现它。

const fruits=[
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
];

function test() {
  // condition: if any fruit is red
  const isAnyRed=fruits.some(f=> f.color=='red');

  console.log(isAnyRed); // true
}



6. 使用可选链和空值合并

这有两个为编写更清晰的条件语句而即将成为 JavaScript 增强的功能。当写这篇文章时,它们还没有被完全支持,你需要使用 Babel 来编译。

可选链允许我们没有明确检查中间节点是否存在地处理 tree-like 结构,空值合并和可选链组合起来工作得很好,以确保为不存在的值赋一个默认值。

这有一个例子:

   const car={
    model: 'Fiesta',
    manufacturer: {
    name: 'Ford',
    address: {
      street: 'Some Street Name',
      number: '5555',
      state: 'USA'
      }
    }
  } 

  // to get the car model
  const model=car && car.model || 'default model';

  // to get the manufacturer street
  const street=car && car.manufacturer && car.manufacturer.address && 
  car.manufacturer.address.street || 'default street';

  // request an un-existing property
  const phoneNumber=car && car.manufacturer && car.manufacturer.address 
  && car.manufacturer.phoneNumber;

  console.log(model) // 'Fiesta'
  console.log(street) // 'Some Street Name'
  console.log(phoneNumber) // undefined


所以,如果我们想要打印是否车辆生产商来自美国,代码将看起来像这样:

 const isManufacturerFromUSA=()=> {
   if(car && car.manufacturer && car.manufacturer.address && 
 car.manufacturer.address.state==='USA') {
     console.log('true');
   }
 }


 checkCarManufacturerState() // 'true'


你能清晰地看到当有一个更复杂的对象结构时,这能变得多乱。有一些第三方的库有它们自己的函数,像 lodash 或 idx。例如 lodash 有 _.get 方法。然而,JavaScript 语言本身被引入这个特性是非常酷的。

这展示了这些新特性如何工作:

 // to get the car model
 const model=car?.model ?? 'default model';

 // to get the manufacturer street
 const street=car?.manufacturer?.address?.street ?? 'default street';

 // to check if the car manufacturer is from the USA
 const isManufacturerFromUSA=()=> {
   if(car?.manufacturer?.address?.state==='USA') {
     console.log('true');
   }
 }


这看起来很美观并容易维护。它已经到 TC39 stage 3 阶段,让我们等待它获得批准,然后我们就能无处不在地看到这难以置信的语法的使用。


总结

让我们为了编写更清晰、易维护的代码,学习并尝试新的技巧和技术,因为在几个月后,长长的条件看起来像搬石头砸自己的脚。