CleanCode-函数

函数

1.短小,函数应该更短小。

每个函数很短,每个函数只说一件事,每个函数都依序把你带到下一个函数。

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) {
    if (isTestPage(pageData)) {
        includeSetupAndTeardownPages(pageData, isSuite);
    }
    return pageData.getHtml();
}

注意代码块和缩进。
if语句、else语句、while语句等,其中的代码块应该只有一行。可以用函数放置代表背后意思,这样容易阅读并保持函数短小。

2.只做一件事

函数应该做一件事,做好这件事。

重构后的代码:将设置和拆解包纳到测试页面上。

背后三个步骤:

1)判断是否是测试页面;
2)如果是,则容纳进设置和分拆步骤
3)渲染成HTML

这三个步骤均在该函数名下的同一抽象层上,本质上是一件事。
重构前代码混乱,包含多个不同抽象层级的步骤。

要判断函数是否不止做了一件事,看看能否再拆出一个函数,而这个函数不仅只是单纯地重新诠释其实现。

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

函数中的语句都要在同一抽象层级上。

getHtml()等位于较高抽象层的概念;
String pagePathName = PathParser.render(pagePath)位于中间抽象层,
还有 .append(“\n”)等位于相当低的抽象层概念。

函数中混杂不同抽象层级,往往让人迷惑,无法判断某个表达式是基础概念还是细节。

自顶向下读代码:向下规则

让每个函数后面都跟着位于下一抽象层级的函数。

4.switch语句

写出短小的switch语句很难,因为switch天生要做N件事情。通过多态可以确保每个switch都埋藏在较低的抽象层级,并永不重复。

如下代码,依赖于雇员类型的操作。

public Money calculatePay(Employee e) throws InvalidEmployeeTye {
	switch(e.type) {
        case COMMISSIONED:
			return calculateCommissionPay(e);
        case HOURLY:
			return calculateHourlyPay(e);
        case SALARIED:
			return calculateSalariedPay(e);
        default:
			throw new InvalidEmployeeType(e.type);
    }
}

该函数的问题:
首先,太长,当出现新的雇员类型,会变得更长;
其次,不止做了一件事情;
第三,违反了SRP单一职责原则,有好几个修改它的理由
第四,违反了OCP开闭原则,每当添加新类型,都必须修改。

该函数最麻烦的可能是导出皆有类似结构的函数,例如,可能有:

isPayday(Empolyee e, Date date),或 deliveryPay(Employee e, Money pay) 等等。

该问题的解决方案,将switch语句埋到抽象工厂底下,不让任何人看到。
该工厂使用switch语句为Employee的派生物创建适当的实体。
不同的函数如 calculatePayisPaydaydeliveryPay等,由 Employee接口多态地接受派遣。

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(e.type) {
        case COMMISSIONED:
			return new CommissionedEmployee(r);
        case HOURLY:
			return new HourlyEmployee(r);
        case SALARIED:
			return new SalariedEmployee(r);
        default:
			throw new InvalidEmployeeType(r.type);
    }
}

5.使用描述性的名称

给每个私有方法取个具有描述性的名称,描述函数做的事情。不要害怕长名称,长但具有描述性的名称比短却令人费解更好。不要害怕花时间取名字。
命名方式要保持一致,使用与模块名一脉相承的短语、名词和动词给函数命名。
使用类似的措辞,依序讲出一个故事

6.函数参数

最理想的参数数量是零,其次是一,再次是二。应尽量避免三。有足够的的特殊理由才能用三个以上的参数。
从概念阅读、代码测试的角度,参数少更方便。

(1)单参数
传入操作单参数,将其转换再输出;
传入事件(event),有输入参数而无输出参数。程序将函数看做一个事件,使用该参数修改系统状态。
如果函数要对输入参数进行转换操作,转换结果应体现为返回值。
StringBuffer transform(StringBuffer in)
void transform(StringBuffer in)更合适。

(2)标识参数
不要向函数传入布尔值。这样函数并非只做了一件事,而应该将函数一分为二,针对两种情况进行操作处理。

(3)二参数

若两个参数并非有任何关联组合,可能二参数比一参数还难懂,省略掉不必要的参数。忽略掉的部分就是本不应该写进去的。
即便是 assertEquals(expected, actual)这种二元函数,也有可能搞错位置。 一般期望的值在前,实际的值在后,遵守约定。
使用二元函数要付出代价,尽量将其转换为一元函数。
可以把一个参数写成类成员变量,从而无需传递,
可以把方法作用在其中一个参数对应的类上,从而只需要另一个参数;
可以分离出新类,在构造器中调用一个参数。

(4)三参数

三参数的排序、琢磨问题很复杂,尽量不要采用三参数。

(5)参数对象

如果函数看来需要两个、三个或三个以上参数,说明其中一些参数应该封装为类了。

Circle makeCircle(double x, double y, double radius);
vs Circle makeCircle(Point center, double radius);
x和y本身就是自己名称所属的某个概念的一部分。

(6)参数列表

向函数传入数量可变的参数,比如 String.format方法:

String.format("%s worked %.2f hours.", name, hours);
这里可变参数可以理解为一个参数,即该函数是二元函数。

public String format(String format, Object... args);
有可变参数的函数可以是一元、二元甚至三元,不要超过3。

(7)动词与关键词

给函数取个好名字,才能更好理解函数的意图,以及参数的顺序和意图。
单参数,函数和参数应该形成良好的动词/名词对应形式。
比如: write(name) ,不管name是什么,都要被 write,也可以更细致 writeField(name),name是一个field。

assertEqual改为 assertExpectedEqualsActual(expected, actual)

7.无副作用

有时候,函数看起来是只做一件事,但还是容易隐藏着做了一些事,比如对自己类中的变量做出改动,甚至改变全局变量。

比如,以下代码:

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

这里的副作用就是 Session.initialize()的调用,这本来是个 check函数,但未暗示会初始化该次对话。由此,当误信了函数名而检查用户有效时,会造成抹除现有回话的风险。
这一副作用造成了一次时序性耦合,checkPassword只能在特定时刻调用,初始化会话是安全的时候调用,如果在不合适的时候调用,会话数据会丢失。
可改名为 checkPasswordAndInitializeSession(),但这样仍然违反了“只做一件事”的规则。

输出参数
参数一般被看做为函数的输入。
有的参数被用作输出而非输入,例如: appendFooter(s)
不清楚这个函数是把s添加在什么东西后面,还是它把什么东西添加了s后面,s是输入还是输出函数,需要重点看函数签名。

public void appendFooter(StringBuffer report)

非OOP编程时,很多时候需要输出参数;
而OOP,因为this也有输出函数的意思。最好修改为:report.appendFooter()

主体在report上,而非传入report参数,因为本质就是输出转化后的report。

总体而言,避免使用输出参数。
如果函数必须要修改某种状态,最好修改所属对象的状态。

8.分隔指令与询问

函数要么做什么事,要么回答什么是,两者不可兼得。
函数应该修改某西乡的状态或返回该对象的有关信息,两样都做会导致混乱。
举例:

public boolean set(String attribute, String value);

该函数设置某个指定属性,如果成功返回true,不存在那个属性返回false。
这样导致了以下语句:

if(set("username","unclebob))...

这是在问username属性值是否已设置为unclebob吗?还是在问username属性值是否成功设置为unclebob呢?很难判断,因为set是动词还是形容词不清楚。

作者本意,set是动词,但在if语句的上下文中,更像形容词。
这个语句读出来更像是**“如果username属性值之前已被设置为uncleob”而不是“设置username属性值为unclebob,看看是否可行,然后…”**

因此,可将set函数重命名为 setAndCheckIfExists,但这对提高if语句可读性帮助不大。真正的解决方案是把指令与询问分隔开,防止混淆。

if(attributeExists("username")) {
	setAttribute("username","unclebob");
    ..
}

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

从指令式函数返回错误码轻微违反了指令与询问分割的规则,还鼓励了在if语句判断中把指令当做表达式使用。

if(deletePage(page) == E_OK)
这不会引起动词/形容词混淆,但容易导致更深层次的嵌套结构,当返回错误码时,就是在要求调用者立刻处理错误。

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("..")}else {
			logger.log("...")
        }
    } else {
        logger.log("...");
		return E_EORROR;
    }
}

如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。

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

(1)抽离Try/Catch代码块

try/catch代码块丑陋不堪,他们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体抽离出来,另外形成函数。

public void delete(Page page) {
    try{
        deletePageAndAllReferences(page);
    } catch(Exception e) {
		logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
	registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
    logger.log(e.getMessage());
}

delete函数只与错误处理有关,很容易理解之后就忽略掉。
deletePageAndAllReference函数只与安全删除一个page有关,错误处理可以忽略掉。

​ (2) 错误处理就是一件事

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

(3)Error.java 依赖磁铁

返回错误码通常暗示某处有个类或枚举,定义了所有错误码。这样的类就像是一块依赖磁铁,所有这些其他的类都需要重新编译和部署。这对Error类造成了负面压力。

public enum Error {
    OK,
	INVALID,
    NO_SUCH,
	LOCKED,
	OUT_OF_RESOURCES,
	WAITING_FOR_EVENT;
}

使用异常替代错误码,新异常可以从异常类派生出来,无需重新编译或重新部署。

10.别重复自己

不要重复自己的代码,代码臃肿,修改出错的机会更大。

11.结构化编程

每个函数、函数中的每个代码块都应该有一个入口、一个出口。
遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且不能有goto语句。
这更适合大函数,小函数可以有不符合。
如果函数保持短小,偶尔出现return、break、continue没有坏处,甚至比单入单出更有表达力。

12.如何写出这样的函数

初稿也许粗陋无序,反复斟酌推敲,直至自己满意。
结合单元测试,分解函数,修改名称,消除重复,缩短和重新安置方法,拆分类等等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值