关于后端:理论与实践如何写好一个方法

8次阅读

共计 10269 个字符,预计需要花费 26 分钟才能阅读完成。

简介:集体认为一个好的办法次要体现在可读性、可维护性、可复用性上,本文通过设计准则和代码标准两章来解说如何进步办法的可读性、可维护性、可复用性。这些设计准则和代码标准更多的是体现一种思维,不仅仅能够用在办法上,也能够用在类上、模块上。上面通过具体的例子来解说。

作者 | 汪军伍 (处轩) 起源 | 阿里开发者公众号集体认为一个好的办法次要体现在可读性、可维护性、可复用性上,本文通过设计准则和代码标准两章来解说如何进步办法的可读性、可维护性、可复用性。这些设计准则和代码标准更多的是体现一种思维,不仅仅能够用在办法上,也能够用在类上、模块上。上面通过具体的例子来解说。设计准则繁多准则繁多职责解释是一个模块只负责实现一个职责或者性能,次要是晋升办法的可维护性和复用性。上面看一个例子:public boolean checkUsernameOrPassword(String str) {

return str != null && str.length() > 5 && str.length() < 18;

}这是一个校验用户名和明码是否非法的办法,它们的实现逻辑一样,很多时候咱们为了简略,就把他们合并成一个。随着业务的倒退,校验明码须要在原有的逻辑上加上要蕴含大小写和数字,如果在原办法根底上,就会影响到校验用户名的逻辑,这是就须要拆分成两个繁多的办法。如下:public boolean checkPassword(String password) {

boolean checkLen = password != null && password.length() > 5 && password.length() < 18;
return checkLen && (校验大小写和数字);

}

public boolean checkUsername(String username) {

return userName != null && userName.length() > 5 && userName.length() < 18;

}如果一开始就满足繁多准则,只需释怀要在“checkPassword”批改就好了,齐全不必放心影响其余业务逻辑。为了举例,用了比较简单的办法,线上的业务逻辑要简单许多,很难一眼就看出影响面。如果一个办法不满足繁多准则,日后别人在批改这块代码时,很容易踩坑,导致线上故障。KISS 准则 Keep It Simple and Stupid,解释是尽量放弃代码简略,次要晋升办法的可读性和可维护性,那么如何了解简略呢?代码简略就是代码行数少?先来看下两种校验 IP 办法:出处:设计模式之美

// 第一种应用正则
public boolean isValidIpAddressV1(String ipAddress) {

if (StringUtils.isBlank(ipAddress)) return false;
String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
    + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
    + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
    + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
return ipAddress.matches(regex);

}

// 第二种应用依据规定逐级判断
public boolean isValidIpAddressV2(String ipAddress) {

if (StringUtils.isBlank(ipAddress)) return false;
String[] ipUnits = StringUtils.split(ipAddress, '.');
if (ipUnits.length != 4) {return false;}
for (int i = 0; i < 4; ++i) {
    int ipUnitIntValue;
    try {ipUnitIntValue = Integer.parseInt(ipUnits[i]);
    } catch (NumberFormatException e) {return false;}
    if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {return false;}
    if (i == 0 && ipUnitIntValue == 0) {return false;}
}
return true;

}很显著尽管第一种办法尽管代码比第二种办法代码行数少了一倍还多,然而并不简略。从实现性、易读性和可维护性来说却比第二种办法难了一倍还多,所以代码行数少并不能代表代码简略。代码简单就违反了 kiss 准则?下面提到,简略并不代表代码行数少,而是从实现性、易读性和可维护性来思考。那么代码简单就违反了 KISS 准则?上面示例一段字符串搜寻两种不同实现形式:出处:知乎《KMP 算法详解》

// 暴力搜寻
int search(String pat, String txt) {

int M = pat.length();
int N = txt.length();
for (int i = 0; i <= N - M; i++) {
    int j;
    for (j = 0; j < M; j++) {if (pat[j] != txt[i+j])
            break;
    }
    // pat 全都匹配了
    if (j == M) return i;
}
// txt 中不存在 pat 子串
return -1;

}

// 应用 KMP
public class KMP {

private int[][] dp;
private String pat;
public KMP(String pat) {
    this.pat = pat;
    int M = pat.length();
    // dp[状态][字符] = 下个状态
    dp = new int[M][256];
    // base case
    dp[0][pat.charAt(0)] = 1;
    // 影子状态 X 初始为 0
    int X = 0;
    // 构建状态转移图(稍改的更紧凑了)for (int j = 1; j < M; j++) {for (int c = 0; c < 256; c++)
            dp[j] = dp[X];
        dp[j][pat.charAt(j)] = j + 1;
        // 更新影子状态
        X = dp[X][pat.charAt(j)];
    }
}
public int search(String pat ,String txt) {int M = pat.length();
    int N = txt.length();
    // pat 的初始态为 0
    int j = 0;
    for (int i = 0; i < N; i++) {
        // 计算 pat 的下一个状态
        j = dp[j][txt.charAt(i)];
        // 达到终止态,返回后果
        if (j == M) return i - M + 1;
    }
    // 没达到终止态,匹配失败
    return -1;
}

}很显著,KMP 实现难度、可读性、可维护性都比第一种高。然而如果在特定的场景,比方解决大文本字符串呈现性能瓶颈的时候,第二种能比第一种效率高 N 倍。而解决性能等艰难的问题,原本就要用更简单的办法,所以并不违反 Kiss 准则。怎么才是满足 KISS 准则 1. 不要适度“炫技”,如果 KMP 只是用来解决平时工作用到的小字符串,那么也是违反 Kiss 准则的。2. 非必要不要应用共事不懂的技术,比方后面例子中的正则表达式,还有一些编程语言中过于高级的语法等。很多刚入职的小伙伴、包含我在刚入职的时候,喜爱搞一些简单的货色来“炫一炫”,放着工具类不必,本人写一堆简单的实现,导致前面代码根本自在本人能力看得懂,还容易出 Bug。其实在满足以后业务场景下,解决问题的办法越简略,反而更能体现一个人的代码能力,援用《重构》一句经典的话来说就是:” 任何一个傻瓜都能写出计算机能够了解的程序,只有写出人类容易了解的程序才是优良的程序员。”DRY 准则 Don’t Repeat Yourself,解释是不要写反复的代码,体现在 实现逻辑反复、性能语义反复和代码执行反复 三个方面,次要晋升办法的可复用性和可维护性。上面具体说一下这三个方面。实现逻辑反复实现逻辑反复,实质是两个办法的代码根本一样,那代码一样那就肯定违反 DRY 吗?先看一个示例:public boolean checkPassword(String password) {

return password != null && password.length() > 6 && password.length() < 18;

}

public boolean checkUsername(String userName) {

return userName != null && userName.length() > 6 && userName.length() < 18;

}下面两个办法去校验用户名和明码,它们实现逻辑雷同,然而性能语义不一样,如果把它们合到一个办法“checkUserNameOrPassWord”外面,一个办法做了两件事,违反了繁多准则,日后随着业务倒退,如果校验明码的规定扭转了,那么就会影响到校验用户名逻辑,又须要从新拆分。实现逻辑雷同,性能语义齐全不一样,并不违反 DRY 准则。实现逻辑不一样就不违反 DRY?还是以校验 IP 为:// 第一种应用正则
public boolean isValidIpAddressV1(String ipAddress) {

if (StringUtils.isBlank(ipAddress)) return false;
String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
    + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
    + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
    + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
return ipAddress.matches(regex);

}

// 第二种应用依据规定逐级判断
public boolean isValidIpAddressV2(String ipAddress) {

if (StringUtils.isBlank(ipAddress)) return false;
String[] ipUnits = StringUtils.split(ipAddress, '.');
if (ipUnits.length != 4) {return false;}
for (int i = 0; i < 4; ++i) {
    int ipUnitIntValue;
    try {ipUnitIntValue = Integer.parseInt(ipUnits[i]);
    } catch (NumberFormatException e) {return false;}
    if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {return false;}
    if (i == 0 && ipUnitIntValue == 0) {return false;}
}
return true;

}下面两个办法实现逻辑齐全不一样,然而性能语义一样,他们的利用场景也基本相同。如果零碎有些中央调了“isValidIpAddressV1”,有些中央调了“isValidIpAddressV2”相当于给代码“埋坑”。1. 扰乱眼帘,减少了解老本:如果两个性能一样,然而实现逻辑不一样代码,会让人感觉是不是有更浅近的考量,才定义了两个性能相似的函数,如果代码开发者在还能答复你是代码设计问题,不然可能又是一处历史谜题,也不敢轻易改变。2. 减少保护老本:如果到时候须要扭转校验 IP 的逻辑,那么很可能改了一个遗记改了另一个,导致系统呈现故障。实现逻辑不一样,然而性能语义齐全一样,也是违反 DRY 准则的。代码执行反复次要体现为一次调用,屡次执行目标雷同的代码。上面看个例子:public class UserService{

@Autowired
private UserManager userManager;
public ServiceResult<User> getUserById(long userId){boolean existed = userManager.checkUserExisted(userId);
    if(!existed){ServiceResult.getFailureResult("code","msg");
    }
    User user = userManager.getUserById(userId);
    return ServiceResult.getSuccessResult(user);
}

}

public static class UserManager {

public User getUserById(long userId){boolean existed = userManager.checkUserExisted(userId);
    if(!existed){// 抛异样}
    return 查询数据库后果;
}


public boolean checkUserExisted(long userId){return 依据肯定规定校验的后果}
}下面这段代码,别离两次的去调用了“checkUserExisted”办法,导致了执行反复,违反了 DRY 准则。个别对于执行反复,能够调整代码程序,对立标准来防止这个问题。比如说对于业务校验,咱们能够放在 Manager 层对立解决。防止执行反复,能够无效晋升办法性能。特地是对于数据库和 RPC 这类 I /O 操作是比拟耗时的,咱们在写代码的时候,该当尽量减少这类 I/O 操作。好的代码习惯只管恪守了下面准则,然而没有好的代码习惯,相似于夸夸其谈,也是不行的。本章来聊一聊写办法时好的代码标准。命名命名是一个既简略又难的问题,简略的命名只有一个单词,而难的中央在于,命名的好坏间接影响到整个代码的可读性。办法的名字贯通着整个办法的思维,如果办法的命名和实现的性能牛头不对马嘴,整个代码读起来事倍功半,甚至会被谬误了解而造成整个零碎的故障,那么怎么命名才适合呢?命名具体越好?上文提到,名字间接贯通整个办法的思维。那我尽可能详细描述办法的用意,哪怕名字因为太长被切成两行也无所谓?相同在可能表白含意的前提下,名字越短越好,太长的命名反而会影响易读性,减少了解老本。上面提几个在不影表白含意的前提下,缩减命名的好的形式。1. 利用上下文缩减单词,如 getUserName,在 User 类这个上文环境下,可缩减成 getName。2. 用缩写来代替,如 to=>2、business=>biz、DailyActiveUse => DAU。3. 查看办法是否足够繁多。在命名完当前,最好以使用者的角度去看下,是否命名通俗易懂。命名要对立你是否在调用 DAO 层查问一个数据的时候,看到“selectXXX”、“queryXXX”、“getXXX”不知用哪一个、搜寻一个办法的时候,应用“selectXXX”搜寻不到,再用“queryXXX”在搜寻一次。如果命名不对立的话,就很容易呈现上述问题,最好项目组定一个对立的标准,在开发和我的项目保护时防止很多问题。参数定义防止参数过多当办法参数大于等于 4 个的时候,参数就有点过多了,次要影响到代码的可读性和易用性,咱们能够看好的框架源码,如 Spring 定义方法入参很少超过三个。如果参数过多,个别有两种办法:1. 以后函数是否满足繁多准则。2. 将多个参数封装为对象。须要留神的是,对外的接口最好都将参数封装成对象,如果须要增加新的参数的时候,接口调用方有可能就不须要批改代码来兼容新的接口了,接口提供方也能够不必重载办法来兼容老接口调用方。“Integer”还是“int”为什么有的办法入参是 Integer 而有的又是 int,定义类型使到底应用 Integer 还是 int?如果不晓得能够依据上面几个类型辨别。1.null 是否有业务意义。比如说更新表字段时,null 代表不更新、查问数据时 null 代表没有。以上场景用 Integer 更适合。2. 参数是否必填,如果必填的话,应用 int 更适合。3. 尊重原有数据类型,防止没必要的拆包。如:Order 中的 id 类型为 Long,必定不为空,但在定义方法入参时,没有必要用 long 类型,能够间接用 Long,防止没必要的拆包。须要留神的是,如果团队平时 Integer 和 int 没特地标准的话,入参最好少用 int,不然容易造成 NPE。办法体办法体多大才适合办法多大适合,每个人都用本人的认识,但我观点是,办法最好不要超过横着的一屏,超过一屏之后,在浏览代码的时候,为串联前后的代码逻辑,就可能须要频繁地上下滚动屏幕,浏览体验不好不说,还容易出错。判断一个办法多大适合,最简略的那就是,当一个办法读了下文让你快忘了上文的时候,根本阐明这个办法代码过多了。代码适当分块对于比拟长的办法,如果逻辑能够分为几个独立的局部,能够利用空行来宰割成几块代码块,每块代码前加上适当的正文,能够让代码的整体构造看起来更加有清晰、有条理。上面贴一段 Spring 源码的“registerBeanPostProcessors”办法代码分块示例:出处:Spring 框架源码

public static void registerBeanPostProcessors(

ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
 // ...
// Register BeanPostProcessorChecker that logs an info message when
// a bean is created during BeanPostProcessor instantiation, i.e. when
// a bean is not eligible for getting processed by all BeanPostProcessors.
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

// Separate between BeanPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
List<BeanPostProcessor> internalPostProcessors = new ArrayList<BeanPostProcessor>();
List<String> orderedPostProcessorNames = new ArrayList<String>();
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
        priorityOrderedPostProcessors.add(pp);
        if (pp instanceof MergedBeanDefinitionPostProcessor) {internalPostProcessors.add(pp);
        }
    }
    else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {orderedPostProcessorNames.add(ppName);
    }
    else {nonOrderedPostProcessorNames.add(ppName);
    }
}

// First, register the BeanPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

// Next, register the BeanPostProcessors that implement Ordered.
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<BeanPostProcessor>();
for (String ppName : orderedPostProcessorNames) {BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
    orderedPostProcessors.add(pp);
    if (pp instanceof MergedBeanDefinitionPostProcessor) {internalPostProcessors.add(pp);
    }
}

//…
}移除过深的嵌套档次如果代码 if 嵌套者 for、for 嵌套着 if,不晓得大家读着什么感触,可能才看到就开始头晕了。嵌套过深的代码一眼看去,高情商说他在叠一座金字塔,低情商说看不出来堆的什么,十分影响易读性。如果嵌套过深,能够尝试上面的解法:1. 利用 return,continue 等关键字 // 革新前
public void example(List<String> strList,String substr) {

if(CollectionUtils.isNotEmpty(strList) && StringUtils.isNotBlank(substr)){for (String str :strList) {if(str != null){if(str.contains(substr))   {// 调用存在业务办法}else {// 调用不存在业务办法}
        }
    }
}

}

// 革新后
public void example(List<String> strList, String substr) {


// 利用 return 提前退出
if (CollectionUtils.isEmpty(strList) || StringUtils.isBlank(substr)) {return;}

for (String str : strList) {
    // 利用 continue,提前结束本次循环
    if (str == null) {continue;}
    
    if (str.contains(substr)) {// 调用业务办法}else {// 调用不存在业务办法}
}

}2. 去除多余的 if-elsepublic void example(String str) {

if (StringUtils.isBlank(str)) {return;} else { // 此处的 else 能够去掉
    // 做其余业务逻辑
}

}3. 抽出局部嵌套逻辑,封装为办法 4. 应用策略 + 工厂等设计模式等定义变量不宜过早在办法开始时,一开始就把所以变量定义好,而后在几十行后在应用,阅读者在浏览这种格调的代码时,一开始不晓得这个参数干嘛用,到前面不晓得这个参数怎么定义的,还要高低滚动屏幕,大大降低了易读性,定义变量的时候,最好在须要它的前一行定义好,上下文之间分割高深莫测。正文正文次要用于一段代码的解析,能够让阅读者更易了解一个办法,所以并没有强制的规定写什么,写多少。如果你感觉名字表白不分明或者须要留神的中央等都能够写在正文里。然而写正文个别须要留神以下几点:1. 不要太依赖正文,这会让你有兜底的想法,而疏忽代码里的可读性。2. 正文写太具体,前面批改代码逻辑时,往往也须要批改正文的内容,而改代码的时候很容易漠视正文,导致正文和代码对不上,大大降低了代码可读性。参考资料和书籍:《设计模式之美》https://time.geekbang.org/col…《重构》https://book.douban.com/subje… 举荐浏览 1. 从业务开发中学习和了解架构设计 2. 代码正文的艺术,优良代码真的不须要正文吗?3. 是什么让一段 20 行代码的性能晋升了 10 倍 GTS 云巧乘风者征文大赛上线!云巧是“组装式利用”理念的落地,助力大家晋升交付速度,进步交付品质,升高用工老本。加入云巧征文大赛,不仅能够让业余导师团评估,更有 888 元猫超卡和天猫精灵 Sound 等你来~前 100 名加入就可取得 38 元天猫超市卡哦,先到先得!点击这里,查看详情。原文链接:https://click.aliyun.com/m/10… 本文为阿里云原创内容,未经容许不得转载。

正文完
 0