案例分析|如何消除代码坏味道
一、背景
开发一款Idea插件,实现对yaml文件的定制化格式检查。- !! 后指定的类路径是否准确
- yaml中的key是否equal类中field的name
- value是否能够转换成类中field的类型
- ……
二、代码比较
2.1命名
一个好的命名能输出更多的信息,它会告诉你,它为什么存在,它是做什么事的,应该怎么使用。2.1.1 类
| 功能 | 时间 | 类名称 |
| 检查yaml文件是否可以成功反序列化成项目中的对象。 | before | YamlBaseInspection |
| after | CeltClassInspection |
类的命名要做到见名知意,before的命名 YamlBaseInspection 做不到这一点,通过类名并不能够获取到有用的信息。对于 CeltClassInspection 的命名格式,在了解插件功能的基础上,可以直接判断出属于yaml类格式检查。
2.1.2 函数
| 功能 | 时间 | 函数名称 |
| 比较value是否可以反序列化成PsiClass | before | compareNameAndValue |
| after | compareKeyAndValue |
2.1.3 变量
//beforeASTNode node = mapping.getNode().findChildByType(YAMLTokenTypes.TAG);String className = node.getText().substring(2);//afterASTNode node = mapping.getNode().findChildByType(YAMLTokenTypes.TAG);String tagClassName = node.getText().substring(2);
String className 来源可以有两个: 1.通过yaml中 tag 标签在项目中查找得到。 2.PsiClass中的变量类型得出。比较:
after:通过变量名 tagClass 可以快速准确的获取变量名属于上述来源中的第一个,能够降低 阅读代码的复杂度 。变量名可以传递更多有用的信息。
2.2 注释
2.2.1 注释格式
- before 1.无注释 2.有注释不符合规范
- after 有注释符合JavaDoc规范
//beforeprivate boolean checkSimpleValue(PsiClass psiClass, PsiElement value)/*** 检查枚举类的value* @return*/boolean checkEnum(PsiClass psiClass,String text)//after/*** @param psiClass* @param value* @return true 正常;false 异常*/private boolean checkSimpleValue(PsiClass psiClass, PsiElement value, ProblemsHolder holder)
2.2.2 注释位置
before://simple类型,检查keyName 和 value格式if (PsiClassUtil.isSimpleType(psiClass)) {//泛型(T)、Object、白名单:不进行检查} else if (PsiClassUtil.isGenericType(psiClass)) {//complex类型} else {}
// simpleValue 为 null 或者 "null"if (YamlUtil.isNull(value)) {}if (PsiClassUtil.isSimpleType(psiClass)) {// simple类型,检查keyName 和 value格式checkSimpleValue(psiClass, value, holder);} else if (PsiClassUtil.isGenericType(psiClass)) {//泛型(T)、Object、白名单:不进行检查} else {checkComplexValue(psiClass, value, holder);}
2.3 方法抽象
before:public void compareNameAndValue(PsiClass psiClass, YAMLValue value) {//simple类型,检查keyName 和 value格式if (PsiClassUtil.isSimpleType(psiClass)) {//泛型(T)、Object、白名单:不进行检查} else if (PsiClassUtil.isGenericType(psiClass)) {//complex类型} else {Map<String, PsiType> map = new HashMap<>();Map<YAMLKeyValue, PsiType> keyValuePsiTypeMap = new HashMap<>();//init Map<KeyValue,PsiType>, 注册keyName Error的错误PsiField[] allFields = psiClass.getAllFields();YAMLMapping mapping = (YAMLMapping) value;Collection<YAMLKeyValue> keyValues = mapping.getKeyValues();for (PsiField field : allFields) {map.put(field.getName(), field.getType());}for (YAMLKeyValue keyValue : keyValues) {if (map.containsKey(keyValue.getName())) {keyValuePsiTypeMap.put(keyValue, map.get(keyValue.getName()));} else {holder.registerProblem(keyValue.getKey(), "找不到这个属性", ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);}}keyValuePsiTypeMap.forEach((yamlKeyValue, psiType) -> {//todo:数组类型type 的 checkif (psiType instanceof PsiArrayType || PsiClassUtil.isCollectionOrMap(PsiTypeUtil.getPsiCLass(psiType, yamlKeyValue))) {} else {compareNameAndValue(PsiTypeUtil.getPsiCLass(psiType, yamlKeyValue), yamlKeyValue.getValue());}});}}
public void compareKeyAndValue(PsiClass psiClass, YAMLValue value, ProblemsHolder holder) {// simpleValue 为 null 或者 "null"if (YamlUtil.isNull(value)) {return;}if (PsiClassUtil.isSimpleType(psiClass)) {// simple类型,检查keyName 和 value格式checkSimpleValue(psiClass, value, holder);} else if (PsiClassUtil.isGenericType(psiClass)) {//泛型(T)、Object、白名单:不进行检查} else {checkComplexValue(psiClass, value, holder);}}boolean checkComplexValue();
2.4 if复杂判断
before
after
3.1 报错信息精准
//beforeholder.registerProblem(value, "类型无法转换", ProblemHighlightType.GENERIC_ERROR);//afterString errorMsg = String.format("cannot find field:%s in class:%s", yamlKeyValue.getName(), psiClass.getQualifiedName());holder.registerProblem(yamlKeyValue.getKey(), errorMsg, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);
3.2 代码健壮性(异常处理)
空指针
before: 代码需要考虑异常(空指针、预期之外的场景),下面代码有空指针异常,deleteSqlList可能为null,3行调用会抛出NPE,程序没有捕获处理。YAMLKeyValue deleteSqlList = mapping.getKeyValueByKey("deleteSQLList");YAMLSequence sequence = (YAMLSequence) deleteSqlList.getValue();List<YAMLSequenceItem> items = sequence.getItems();for (YAMLSequenceItem item : items) {if (!DELETE_SQL_PATTERN.matcher(item.getValue().getText()).find()) {holder.registerProblem(item.getValue(), "sql error", ProblemHighlightType.GENERIC_ERROR);}}
@Overridepublic void doVisitMapping(@NotNull YAMLMapping mapping, @NotNull ProblemsHolder holder) {ASTNode node = mapping.getNode().findChildByType(YAMLTokenTypes.TAG);//取出nodeif (YamlUtil.isNull(node)) {return;}if (node.getText() == null || !node.getText().startsWith("!!")) {// throw new RuntimeException("yaml插件监测异常,YAMLQuotedTextImpl text is null或者不是!!开头");holder.registerProblem(node.getPsi(), "yaml插件监测异常,YAMLQuotedTextImpl text is null或者不是!!开头", ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);return;}String tagClassName = node.getText().substring(2);PsiClass[] psiClasses = ProjectService.findPsiClasses(tagClassName, mapping.getProject());if (ArrayUtils.isEmpty(psiClasses)) {String errorMsg = String.format("cannot find className = %s", tagClassName);holder.registerProblem(node.getPsi(), errorMsg, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);return;}if (psiClasses.length == 1) {compareKeyAndValue(psiClasses[0], mapping, holder);}}
switch中的default
before:switch (className) {case "java.lang.Boolean":break;case "java.lang.Character":break;case "java.math.BigDecimal":break;case "java.util.Date":break;default:}
switch (className) {case "java.lang.Boolean":break;case "java.lang.Character":break;case "java.math.BigDecimal":break;case "java.util.Date":case "java.lang.String":return true;default:holder.registerProblem(value, "未识别的className:" +className, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL);return false;}
比较:
before:代码存在 隐藏逻辑 String类型会走default逻辑不处理,增加 代码理解的难度 。未对非simple类型的default有异常处理。 after:对String类型写到具体case, 暴漏隐藏逻辑 。并对default做异常处理,代码更 健壮 。