• 代码整洁之道-读书笔记之函数


    1.短小

    搞定了函数的命名之后,看一下函数内容的建议和规范,第一原则:简短

    问:函数应该有多么短小呢?

    答:if语句、else语句、while语句等,其中的代码块应该只有一行

    函数的缩进层级不应该多余一层或者两层,这样的函数易于阅读和理解

    2.只做一件事

    一函数理论上只做一件事情,只做一个抽象层次的事情,通俗的说就是看看当前函数是否还可以拆分出一个函数,如果可以说明就不是做一件事

    3.每个函数一个抽象层级

    保证一个函数一个抽象层级,也是确保函数只做一件事的依据。

    阅读代码的习惯:自顶向下阅读
    在这里插入图片描述

    4.switch语句

    switch语句的本意就是完成多件事情,下面看一段switch的代码

    public Money calculatePay(Employee e) throws InvalidEmployeeType{
    	switch(e.type){ 
    		case COMMISSIONED: // 正式员工
    			return calculateCommissionedPay(e); 
    		case HOURLY: // 小时工
    			return calculateHourlyPay(e); 
    		case SALARIED: // 农民工
    			return calculateSalariedPay(e); 
    		default:
    			throw new InvalidEmployeeType (e.type); 
    }
    

    大家看到这个有人认为代码比较清爽,也比较简洁。

    其实这里存在几个

    1.当出现新的员工类型的时候,这里需要添加新的case和新的工资计算的方法

    2.很明显这个方法做了多件事情

    3.违反了单一原则

    4.违反了开闭原则

    在这里我给出上面的问题一个通用的解法:工厂+多态+封装

    public abstract class Employee {
    	public abstract boolean isPayday(); 
    	public abstract Money calculatePay(); 
    	public abstract void deliverPay(Money pay); 
    }
    
    public interface EmployeeFactory{
    	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; 
    }
    
    public class EmployeeFactoryImpl implements EmployeeFactory{
    	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { 
    		switch(r.type){
    			case COMMISSIONED:
    				return new CommissionedEmployee(r); 
    			case HOURLY:
    				return new HourlyEmployee(r); 
    			case SALARIED:
    				return new SalariedEmploye(r); 
    			default:
    				throw new InvalidEmployeeType (r.type); 
    }
    

    5使用描述性的名称

    1.不要害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好,要比描述性的长注释好

    2.不要害怕花时间取名字

    3.命令方式要保持一致,使用与模块名一脉相承的短语、名称、动词

    6.函数参数

    函数参数的数量:0>1>2>3 应该避免3个以及以上

    随着参数数量的增加,单测的组合就越多,函数的就更无法保持只做一件事的标准

    6.1一元函数

    1.单纯操作参数,进行操作 User getUser(long userId);

    2.操作参数,进行转换,并且返回值void void appendString(StringBuilder sb)(慎用)

    3.操作参数,进行转换,将转换后的数据进行返回StringBuilder appendString(StringBuilder sb)

    6.2标识参数

    参数是boolean类型的方法

    例如:operate(boolean flag)

    这种方法会让用户产出模糊的想法,到底是做还是不做,这种的一般拆分成

    canOperate(),noOperate();

    6.3 二元函数

    含有两个参数的函数,比一个参数的难懂一些,但是有的时候也是可以使用两个参数比如copyArray(String[] source, String[] target)

    6.4 三元函数

    含有三个参数的函数,可读性就更差了,创建三个参数的函数的时候,一定要思考情况在进行创建

    6.5 参数对象

    如果一个函数的参数数量过多,建议封装成一个对象进行传递

    例如

    Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point center, double radius);
    

    6.6 参数列表

    有时,我们想要向函数传入数量可变的参数。例如,String.format方法:String.format(“%s worked %.2f hours.”, name, hours);

    如果可变参数像上例中那样被同等对待,就和类型为List的单个参数没什么两样。这样一来,String.formate 实则是二元函数。下列String.format的声明也很明显是二元的:

    public String format(String format, Object... args)
    

    同理,有可变参数的函数可能是一元、二元甚至三元。超过这个数量就可能要犯错了。

    void monad(Integer... args);
    void dyad(String name, Integer... args);
    void triad(String name, int count, Integer... args); 
    

    7 无副作用

    副作用指得就是函数违背了只做一件事的承诺

    下面来看一段代码

    public class UserValidator {
    	private Cryptographer cryptographer;
    	public boolean checkPassword(String userName, String password){ 
    		User user = UserGateway. findByName (userName) ;
    			if (user != User.NULL){
    				String codedPhrase = user.getPhraseEncodedByPassword() ;
    				String phrase = cryptographer.decrypt (codedPhrase, password); 
    			if ("Valid Password".equals (phrase)){
    				Session.initialize();
    				return true;
    			} 
    		}
    		return false; 
    	}
    }
    

    上面代码存在的问题,方法名说的是检查用户密码,但是同时包含了初始化session的功能,

    这样就让这段代码出现了副作用,可能就会在某些情况调用的时候产生bug,也违背了我们一个函数只做一件事的初衷,如果必须要这么做,我们可以考虑重命名为checkPasswordAndInitSession

    输出参数

    参数很自然就会当做函数的输入,但是也有情况是作为输出。例如

    void appendFooter(String s) // 追加页脚
    

    这时候读者就会有疑问,s是添加到什么后面,还是把什么东西添加到s后面,s是函数的输入还是最终的输出,副作用就显露出来了,修改后如下

    report.appendFooter() // 报告追加页脚
    

    8. 分隔指令和询问

    函数要么做什么事情、要么回答什么事情,二者不可兼得

    接下来看一个例子,一个函数修改某一个属性,修改成功就返回true,失败就返回false,如果不存在属性就返回false

    public boolean set(String attribute, String value){
    }
    
    public void test(){
    	if(set("username", "java")){
    		xxxx
    	}
    }
    

    看到上面的代码就会存在疑惑,这里是查看username之前就设置为java呢,还是将username设置成java呢?正确的代码如下:

    if(attributeExists("username")){
    	setAttribute("username", "java")
    }
    

    9使用异常替代返回错误码

    上一小节鼓励我们在if的时候进行判断,在执行业务逻辑,但是这样却会导致我们代码的嵌套结构变深,导致代码可读性下降

    下面看一个例子:

    if (deletePage (page) == E_OK) {
    	if (registry.deleteReference (page.name) == E_OK) {
    		if (configKeys.deleteKey (page.name.makeKey ()) == E_OK) {
    			 logger.log ("page deleted");
    		}else {
    			 logger.log ("configKey not deleted");
    	}else {
    		logger.log ("deleteReference from registry failed");
    }else{
    	logger.log ("delete failed"); 
    	return E_ERROR;
    }
    

    碰到上情况我们可以作如下操作

    try{
    	deletePage(page);
    	registry.deleteReference(page.name);
    	configKeys.deleteKey(page.name.makeKey())
    }catch(Execption e){
    	logger.log(e.getMessage())
    }
    
    

    9.1 抽离try/catch代码块

    try/catch代码非常丑陋,而且我们把错误和正常流程一块处理

    第一种提取方式

    public void delete(Page page){
    	try{
    		deletePageAndA11References (page) ; 
    	}catch(Exception e){ 
    	logError(e);
    	}
    }
    
    private void deletePageAndAl1References (Page page) throws Exception{ 
    	deletePage(page);
    	registry.deleteReference (page. name) ; 
    	configKeys.deleteKey(page. name.makeKey());
    }
    
    private void logError(Exception e){ 
    	logger. log (e.getMessage());
    } 
    

    第二种提取方式

    if (deletePage (page) != E_OK) {
    	logger.log ("delete failed"); 
    	return E_ERROR;
    }
    if (registry.deleteReference(page.name) != E_OK) {
    	logger.log ("deleteReference from registry failed"); 
    	return E_ERROR;
    }
    if (configKeys.deleteKey(page.name.makeKey()) != E_OK) {
    	logger.log ("page deleted");
    	return E_ERROR;
    }
    

    9.2错误处理就是一件事

    函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。

    10 别重复自己

    工程里面不要有重复的代码,如果发现一定要通过重构的手段将其消灭

    11 结构化编程

    结构化编程:一个函数只有一个入口和一个出口,只存在一个return,循环中不能有break和continue

    如果我们可以保持函数的短小,不用遵循上面的原则

    12.如何写出这样的函数

    我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。

    然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。

    最后,遵循本章列出的规则,我组装好这些函数。

    我并不从一开始就按照规则写函数。我想没人做得到。

    13 小结

    本章主要围绕如何写一个好的函数进行讲解

    1.函数要短小

    2.只做一件事

    3.参数尽量要少

    4.尽量避免副作用

    5.异常和正常逻辑要隔离

    6.不要重复

  • 相关阅读:
    无胁科技-TVD每日漏洞情报-2022-10-28
    【ML】基于机器学习的心脏病预测研究(附代码和数据集,逻辑回归模型)
    1.在官网选择版本并下载
    k8s分布式图床(k8s,metricsapi,vue3+ts)
    植物大战僵尸杂交版破解C++实现
    C++(QT)画图行车
    【LeetCode】每日一题&&两数之和&&寻找正序数组的中位数&&找出字符串中第一个匹配项的下标&&在排序数组中查找元素的第一个和最后一个位置
    AI量化(代码):深度强化学习DRL应用于金融量化
    Dubbo源码-Provider服务端ServiceBean初始化和属性注入
    跳表C语言
  • 原文地址:https://blog.csdn.net/constant_rain/article/details/127120194