函数

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.如何写出这样的函数

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

CleanCode-函数相关推荐

  1. 数据库中自定义排序规则,Mysql中自定义字段排序规则,Oracle中自定义字段排序规则,decode函数的用法,field函数的用法

    数据库中自定义排序 场景:有一张banner表,表中有一个status字段,有0, 1, 2三个状态位,我想要 1,0,2的自定义排序(这里是重点),然后再进行之上对sequence字段进行二次排序( ...

  2. Mysql函数group_concat、find_in_set 多值分隔字符字段进行数据库字段值翻译

    Mysql函数group_concat.find_in_set进行数据库字段值翻译 场景 配方表:记录包含的原料 sources表示原料,字段值之间用逗号分隔 原料表:对应原料id和原料名称 现需要查 ...

  3. C++ 笔记(34)— C++ exit 函数

    当遇到 main 函数中的 return 语句时,C++ 程序将停止执行.但其他函数结束时,程序并不会停止.程序的控制将返回到函数调用之后的位置.然而,有时候会出现一些非常少见的情况,使得程序有必要在 ...

  4. C++ 笔记(30)— 友元函数与友元类

    我们知道类的私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行.这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦. ...

  5. 浅显易懂 Makefile 入门 (07)— 其它函数(foreach 、if、call、origin )

    1. foreach 函数 foreach 函数定义如下: $(foreach <var>,<list>,<text>) 函数的功能是:把参数 <list&g ...

  6. 浅显易懂 Makefile 入门 (06)— 文件名操作函数(dir、notdir、suffix、basename、addsuffix、addperfix、join、wildcard)

    编写 Makefile 的时候,很多情况下需要对文件名进行操作.例如获取文件的路径,去除文件的路径,取出文件前缀或后缀等等. 注意:下面的每个函数的参数字符串都会被当作或是一个系列的文件名来看待. 1 ...

  7. Go 学习笔记(65)— Go 中函数参数是传值还是传引用

    Go 语言中,函数参数传递采用是值传递的方式.所谓"值传递",就是将实际参数在内存中的表示逐位拷贝到形式参数中.对于像整型.数组.结构体这类类型,它们的内存表示就是它们自身的数据内 ...

  8. Go 学习笔记(61)— Go 高阶函数、函数作为一等公民(函数作为输入参数、返回值、变量)的写法

    函数在 Go 语言中属于"一等公民(First-Class Citizen)"拥有"一等公民"待遇的语法元素可以如下使用 可以存储在变量中: 可以作为参数传递给 ...

  9. C++ 笔记(26)— 主函数 main(int argc, char *argv[]) 参数说明

    带形参的 main 函数,如 int main( int argc, char* argv[], char **env ) 是 UNIX .Linux 以及 Mac OS 操作系统中 C/C++ 的 ...

  10. OpenCV 笔记(09)— 常用的数据结构和函数(Vec、Point、Scalar、Size、Rect、cvtColor)

    1. Vec 对象类型 Vec 是一个主要用于数值向量的模板类.我们可以定义向量的类型和组件的数量: Vec<double, 19> myVector 我们还可以使用任何的预定义类型: t ...

最新文章

  1. ISP、主机之间的通信方式、电路交换和分组交换、时延
  2. server2016安装mysql_windows server2016安装MySQL5.7.19解压缩版教程详解
  3. 1.QT中的容器QVector,QList,QSet,QMap,QQueue,QStack,QMultiMap,QSingleList等
  4. Mac 编译报错 symbol(s) not found for
  5. 算法动画 - 理解函数曲线
  6. Tomcat logs 目录下各日志文件的含义
  7. 分析SpinnerActivityTest中有关控件操作以及UI线程问题
  8. 【物联网智能网关-08】TinyGUI和WPF汉字显示技术比较
  9. Corona Enterprise 引入第三方 jar 包
  10. 对ichartjs基于基于HTML5的开源图形组件画图的心得
  11. 自由曲面透镜设计matlab,实现LED台灯均匀照明的自由曲面透镜仿真设计
  12. 星际迷航中企业号的动力系统_五月四日与您同在:《星球大战》(和《星际迷航》)如何激发现实生活中的技术...
  13. JVM中的Xms和Xmx
  14. 打开outlook显示服务器内存不足,outlook无法启动,总说计算机内存不足或磁盘已满,是怎么回事?...
  15. 生活记录:记录2020暑假前半部分时光
  16. Office:你的 OneDrive 帐户存在问题?
  17. 【办公自动化】基于Python开发的PDF批量转换-合并应用程序
  18. 如何有效的记忆和提取记忆
  19. 国王游戏(贪心+模拟)
  20. 1-1HTML笔记总结

热门文章

  1. 容错性低是什么意思_王者荣耀:在成为高手之前,这4位容错率低的千万别碰!...
  2. Lodop云打印控件使用
  3. 分享一款国产并口PSRAM存储芯片EMI164NA16LM
  4. [zabbix]cat ‘/home/cjw/create (1).sql/data‘ | mysql -uzabbix -p123456 -Dzabbix ERROR 1049 (42000):
  5. 自然语言处理--keras实现一维卷积网络对IMDB 电影评论数据集构建情感分类器
  6. 微信小程序 MinUI 组件库系列之 price 价格组件
  7. 第 8 篇、Linux C 基础 | 运算符
  8. 综合实验:LVS+LAMP+NFS+MySQL读写分离
  9. VMware 安装失败解决方案,亲测有效
  10. Nginx之proxy_pass详解