《代码整洁之道 》第三章 函数

第三章 函数

短小

函数应该短小,20行封顶。
代码块和缩进
if、else、while等,应该只有一行。

只做一件事

函数应该只做一件事
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函 数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。
所以,要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数, 该函数不仅只是单纯地重新诠释其实现。

每个函数一个抽象层级

要确保函数只做一件事,函数的语句都要在同一抽象层级上。
如果函数中混杂着不同的抽象,会使人迷惑。
自顶向下读代码:向下原则
要让每个函数后面都跟着位于下一抽象层级的函数,这样在看函数列表时,就能遵循抽象层级向下阅读了。

Switch语句

学出短小的Switch很难,因为他天生就是用来处理多件事。
不过还是能够确保每个Switch都埋藏在较低抽象层级,而且永远不会重复。我们可以使用多态来实现这一点。
image.png

  1. 太长了,有新的雇员类型的时候,还会更长
  2. 明显做了不止一件事
  3. 违反了单一权责原则
    1. 如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。
  4. 违法了开闭原则,因为每当添加新类型时,就需要需改。
    1. 面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
  5. 最麻烦的可能是到处皆有类似结构的函数
    1. isPayday(Employee e, Date date), 或者 deliverPay(Employee e, Date date)

解决方案:
image.png

使用描述性的名称

在代码清单3-7中,我把示例函数的名称从testableHtml 改为SetupTeardownIncluder.render 。 这个名称好得多,因为它较好地描述了函数做的事。
我也给每个私有方法取个同样具有描述性 的名称,如isTestable 或includeSetupAndTeardownPages 好名称的价值怎么好评都不为过。
长而具有描述性的名称,要比短而令人费解的名称好,也比描述性的长注释好。
使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
选择描述性的名称能理清你关于模块的设计思路。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如, includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage和includeSetupPage 等。这些名称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上述函数序列,你就会自问:“includeTeardownPages 、includeSuiteTeardownPages 和includeTeardownPage 又会如何?”这就是所“深合己意”了。

函数参数

最理想的参数是没有参数,其次是1个参数,再其次是2个参数。尽量避免三个或三个以上的参数。
参数具有太多的概念性,参数与函数名处在不同的抽象层级,他要求你了解目前并不特别重要的细节。
多个参数,在测试方面也很难测试。
输出参数比输入参数更难理解。

一元函数的普遍形式

向函数传入单个参数有两种极普遍的理由。你也许会问关于那个参数的问题,就像在 boolean fileExists ("MyFile ")中那样。也可能是操作该参数,将其转换为其他什么东西,再输 出之。例如,InputStream fileOpen ("MyFile ")把String 类型的文件名转换为InputStream 类型的 返回值。这就是读者看到函数时所期待的东西。你应当选用较能区别这两种理由的名称,而 且总在一致的上下文中使用这两种形式。
还有一种虽不那么普遍但仍极有用的单参数函数形式,那就是事件(event )。在这种形 式中,有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,
例如void passwordAttemptFailedNtimes (int attempts )小心使用这种形式。应该让读者很清楚 地了解它是个事件。谨慎地选用名称和上下文语境。
尽量避免编写不遵循这些形式的一元函数,
例如,void includeSetupPagelnto (StringBuffer pageText )。对于转换,使用输出参数而非返回值令人迷惑。如果函数要对输入参数进行转换 操作,转换结果就该体现为返回值。
实际上,StringBufifer transform (String Buffer in )要比void transform (StringBuffer out)强,即便第一种形式只简单地返回输参数也是这样。至少,它遵循 了转换的形式。

标识参数

千万不要使用标识参数,因为这样就等相当于宣布该函数不止做了一件事。

二元函数

有两个参数的函数要比一个参数的函数难懂。例如:writeField( name) writeField(outputStream,name)。第一个参数可能一下就明白了,但是第二个函数却需要思考一下。
当然,有些时候两个参数正好。例如,Point p =new Point (0,0)就相当合理笛卡儿点天 生拥有两个参数。如果看到new Point (0),我们会倍感惊讶。然而,本例中的两个参数却只是 单个值的有序组成部分!而output -Stream 和name则既非自然的组合,也不是自然的排序。
即便是如assertEquals (expected ,actual )这样的二元函数也有其问题。你有多少次会搞错 actual 和expectedI 的位置呢?这两个参数没有自然的顺序。expected 在前,actual 在后,只是 一种需要学习的约定黑了。
应该尽量把二员函数转化为一元函数,例如,可以把writeField 方法写成outputStream 的成员之一,从而能这样用:outputStream .writeField (name)。或者,也可以把 outputStream 写成当前类的成员变量,从而无需再传递它。还可以分离出类似FieldWriter 的 新类,在其构造器中采用outputStream ,并且包含一个write 方法。

三元函数

有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问题都会加倍体现。建议你在写三元函数前一定要想清楚。
例如,设想assertEquals 有三个参数:assertEquals (message,expected ,actual))。有多少次, 你读到message ,错以为它是expected 呢?我就常栽在这个三元函数上。实际上,每次我看到这里,总会绕半天圈子,最后学会了忽略message 参数。
另一方面,这里有个并不那么险恶的三元函数:assertEquals (1.0,amoun ,.001)虽然也 要费点神,还是值得的。得到“浮点值的等值是相对而言”的提示总是好的。

参数对象

如果函数是三个或以上的,有些参数就需要封装成类了。
例如

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

减少了参数的数量,当一组参数被共同传递,就像上面的x,y。往往就是该有自己名称的某个概念的一部分。

参数列表

有时候,我们想要向函数传入数量可变的参数,例如String.format方法。
public String format( String format, Object… . args);
但是这样子写,其实和类型为List的单个参数没什么区别。而且这样子写也不太好

动词与关键字

给函数取个好名字,能好地解释函数的意图,以及参数的顺序和意图。对于一元函数, 函数和参数应当形成一种非常良好的动词/名词对形式。例如,wnite (name)就相当令人认同。 不管这个“name”是什么,都要被“write’ ”。更好的名称大概是writeField (name),它告诉我 们,“name”是一个“field ”。
例如:assertEqual 改成 assertExpectedEqualsActual( expected, actual)可能会好些。减少了记忆的负担。

无副作用

副作用是一个谎言。
函数承诺只做一件事,但还是会做其他被他藏起来的事。
image.png

输出参数
参数多数会自热而然的当做是输入参数。
但是这个参数呢:appendFooter(s),是把s添加到什么东西后面吗?
函数签名:public void appendFooter( StringBuffer report)。需要你花时间去阅读函数签名,这样也是不太好的。
其实可以修改成这样 report.appendFooter()
普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。

分隔指令与询问

函数要么做什么事,要么回答什么事,二者不可兼得。
函数应该修改某个对象的状态,或者返回改对象的有关信息,
public boolean set( String attribute, String value);
改函数设置某个指定属性,如果成功就返回true,如果不存在那个属性,就返回false。
就导致会有这种语句:if (set( " username" , " unclebob"))
从读者的角度考虑一下吧。这是什么意思呢?它是在问username/属性值是否之前已设置 为unclebob 吗?或者它是在问username 属性值是否成功设置为unclebob 呢?从这行调用很 难判断其含义,因为set是动词还是形容词并不清楚。
解决方案应该是
image.png

使用异常代替返回错误码

从指令式函数返回错误码轻微违反了指令与询问分隔的规则,他鼓励了在if语句判断中把指令当做表达式使用。
if (deletePage(page)=E_OK)
这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。
image.png

如果使用异常代替返回错误码,错误处理代码就能从主路代码中分离出来。
image.png

抽离Try/Catch代码块

try/catch代码丑陋不堪,把代码结构都搞乱了,最好可以将他剥离,另外形成函数
image.png

错误处理就是一件事

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

Error.java 依赖磁铁

返回错误码,通常暗示某处有个类或是枚举,定义了所有的错误码
image.png

别重复自己

把重复的代码抽象成一个方法,
重复可能是软件中一切邪恶的根源,许多原则与实践都是为控制与消除重复创建的
比如数据库范式、面向对象把代码集中到基类

结构化编程

Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。
意味着,只能有一个return语句,循环中不能有breck、continue,更不能有goto语句。
但是对于小函数,这些规则帮助不大,只有在大函数,这些规则才会有明显的好处。
只要函数足够小,偶尔出现的return、breck、continue没有坏处,甚至比单入单出更有表达力。

如何写出这样的函数

写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。 初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。
我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。
然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。
最后,遵循本章列出的规则,我组装好这些函数。
我并不从一开始就按照规则写函数。我想没人做得到。

小结

每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设计来描述那个系统的。函数是语言的动词,类是名词。这并非是退回到那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可怕的旧观念。其实这是个历史更久的真理。编程艺术是且一直就是语言设计的艺术。
大师级别的程序员会把系统当做故事来讲。
真正的目的是在于讲述系统,而你编写的函数必须干净利落的拼接到一起,形成一种精确而清晰的语音,帮助你讲故事。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值