搞定了函数的命名之后,看一下函数内容的建议和规范,第一原则:简短
问:函数应该有多么短小呢?
答:if语句、else语句、while语句等,其中的代码块应该只有一行
函数的缩进层级不应该多余一层或者两层,这样的函数易于阅读和理解
一函数理论上只做一件事情,只做一个抽象层次的事情,通俗的说就是看看当前函数是否还可以拆分出一个函数,如果可以说明就不是做一件事
保证一个函数一个抽象层级,也是确保函数只做一件事的依据。
阅读代码的习惯:自顶向下阅读
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);
}
1.不要害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好,要比描述性的长注释好
2.不要害怕花时间取名字
3.命令方式要保持一致,使用与模块名一脉相承的短语、名称、动词
函数参数的数量:0>1>2>3 应该避免3个以及以上
随着参数数量的增加,单测的组合就越多,函数的就更无法保持只做一件事的标准
1.单纯操作参数,进行操作 User getUser(long userId);
2.操作参数,进行转换,并且返回值void void appendString(StringBuilder sb)(慎用)
3.操作参数,进行转换,将转换后的数据进行返回StringBuilder appendString(StringBuilder sb)
参数是boolean类型的方法
例如:operate(boolean flag)
这种方法会让用户产出模糊的想法,到底是做还是不做,这种的一般拆分成
canOperate(),noOperate();
含有两个参数的函数,比一个参数的难懂一些,但是有的时候也是可以使用两个参数比如copyArray(String[] source, String[] target)
含有三个参数的函数,可读性就更差了,创建三个参数的函数的时候,一定要思考情况在进行创建
如果一个函数的参数数量过多,建议封装成一个对象进行传递
例如
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
有时,我们想要向函数传入数量可变的参数。例如,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);
副作用指得就是函数违背了只做一件事的承诺
下面来看一段代码
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() // 报告追加页脚
函数要么做什么事情、要么回答什么事情,二者不可兼得
接下来看一个例子,一个函数修改某一个属性,修改成功就返回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")
}
上一小节鼓励我们在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())
}
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;
}
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
工程里面不要有重复的代码,如果发现一定要通过重构的手段将其消灭
结构化编程:一个函数只有一个入口和一个出口,只存在一个return,循环中不能有break和continue
如果我们可以保持函数的短小,不用遵循上面的原则
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。
然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。
最后,遵循本章列出的规则,我组装好这些函数。
我并不从一开始就按照规则写函数。我想没人做得到。
本章主要围绕如何写一个好的函数进行讲解
1.函数要短小
2.只做一件事
3.参数尽量要少
4.尽量避免副作用
5.异常和正常逻辑要隔离
6.不要重复