代码整洁之道(第3章节)--函数
目录
3.1 短小
3.2 只做一件事
3.3 每个函数一个抽象层级
3.4switch语句
3.5 使用描述性的名称
3.6 函数参数
3.6.1 一元函数的普遍形式
3.6.2标识参数
3.6.3 二元函数
3.6.4 三元函数
3.6.5参数对象
3.6.6参数列表
3.6.7动词与关键字
3.8分隔指令与询问
3.9使用异常替代返回错误码
3.9.1抽离TryLatch代码块
3.9.2错误处理就是一件事
3.11结构化编程
3.1 短小
函数的第一规则是要短小。第二条规则是还要更短小。我无法证明这个断言。我给不出任何证实了小函数更好的研究结果。我能说的是,近40年来,我写过各种不同大小的函数。
我写过令人憎恶的长达3000行的厌物,也写过许多100行到300行的函数,我还写过20行到30行的。经过漫长的试错,经验告诉我,函数就该小。
在20世纪80年代,我们常说函数不该长于一屏。当然,说这话的时候,VT100屏幕只有24行、80列,而编辑器就得先占去4行空间放菜单。如今,用上了精致的字体和宽大的显示器,一屏里面可以显示100行,每行能容纳150个字符。每行都不应该有150个字符那么长。函数也不该有100行那么长,20行封顶最佳。
3.2 只做一件事
函数应该做一件事。做好这件事。只做这一件事。
3.3 每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。
函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
自顶向下读代码:
向下规则我们想要让代码拥有自顶向下的阅读顺序。'我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。我把这叫做向下规则。
3.4switch语句
写出短小的switch语句很难'。即便是只有两种条件的switch语句也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。
请看代码清单34。它呈现了可能依赖于雇员类型的仅仅一种操作。
代码清单3-4 Payroll,javapublic 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)}
}
该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责原则(Single Responsibility Principle,SRP),因为有好几个修改它的理由。第四,它违反了开放闭合原则(Open Closed Principle,OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。例如,可能会有
isPayday (Employee e, Date date),
或
deliverPay (Employee e, Money pay),
如此等等。它们的结构都有同样的问题。
该问题的解决方案(如代码清单3-5所示)是将swit©h语句埋到抽象工厂底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则藉由Employee接口多态地接受派遣。
对于switch语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍。当然也要就事论事,有时我也会部分或全部违反这条规矩。
代码清单3-5.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 (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);}}
}
3.5 使用描述性的名称
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
别害怕花时间取名字。你当尝试不同的名称,实测其阅读效果。在Eclipse或Intelli山等现代DE中改名称易如反掌。使用这些DE测试不同名称,直至找到最具有描述性的那一个为止。
选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPages,includeSetupPages,includeSuiteSetupPage includeSetupPage等。这些名称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上述函数序列,你就会自问:“includeTeardownPages、includeSuiteTeardownPages和includeTeardownPage又会如何?”这就是所谓“深合己意”了。
3.6 函数参数
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)一所以无论如何也不要这么做。
参数不易对付。它们带有太多概念性。所以我在代码范例中几乎不加参数。比如,以StringBuffer为例,我们可能不把它作为实体变量,而是当作参数来传递,那样的话,读者每次看到它都得要翻译一遍。阅读模块所讲述的故事时,includeSetupPage()includeSetupPageinto(newPage-Content)易于理解。参数与函数名处在不同的抽象层级,它要求你了解目前并不特别重要的细节(即那个StringBuffer)。
3.6.1 一元函数的普遍形式
向函数传入单个参数有两种极普遍的理由。你也许会问关于那个参数的问题,就像在boolean fileExists("MyFile'")中那样。也可能是操作该参数,将其转换为其他什么东西,再输出之。例如,InputStream fileOpen("MyFile")把String类型的文件名转换为InputStream类型的返回值。这就是读者看到函数时所期待的东西。你应当选用较能区别这两种理由的名称,而且总在一致的上下文中使用这两种形式。
还有一种虽不那么普遍但仍极有用的单参数函数形式,那就是事件(event)。在这种形式中,有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,例如void passwordAttemptFailedNtimes(int attempts)。小心使用这种形式。应该让读者很清楚地了解它是个事件。谨慎地选用名称和上下文语境。
尽量避免编写不遵循这些形式的一元函数,例如,void includeSetupPagelnto(StringBufferpageText)。对于转换,使用输出参数而非返回值令人迷惑。如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。实际上,StringBuffer transform(StringBuffer in)要比voidtransform(StringBuffer out)强,即便第一种形式只简单地返回输参数也是这样。至少,它遵循了转换的形式。
3.6.2标识参数
标识参数丑陋不堪。向函数传入布尔值简直就是骇人听闻的做法。这样做,方法签名立刻变得复杂起来,大声宣布本函数不止做一件事。如果标识为true将会这样做,标识为false则会那样做!
在代码清单3-7中,我们别无选择,因为调用者已经传入了那个标识,而我想把重构范围限制在该函数及该函数以下范围之内。方法调用render((true)对于可怜的读者来说仍然摸不着头脑。卷动屏幕,看到render((Boolean isSuite),稍许有点帮助,不过仍然不够。应该把该函数一分为二:reanderForSuite()和renderForSingleTest()。
3.6.3 二元函数
有两个参数的函数要比一元函数难懂。例如,writeField(name)比writeField(outputStream,name)'好懂。
尽管两种情况下意义都很清楚,但第一个只要扫一眼就明白,更好地表达了其意义。第二个就得暂停一下才能明白,除非我们学会忽略第一个参数。而且最终那也会导致问题,因为我们根本就不该忽略任何代码。忽略掉的部分就是缺陷藏身之地。
当然,有些时候两个参数正好。例如,Pointp=new Point(0,O);就相当合理。笛卡儿点天生拥有两个参数。如果看到new Point(O),我们会倍感惊讶。然而,本例中的两个参数却只是单个值的有序组成部分!而output--Stream和name则既非自然的组合,也不是自然的排序。
即便是如assertEquals(expected,.actual)这样的二元函数也有其问题。你有多少次会搞错actual和expected的位置呢?这两个参数没有自然的顺序。expected在前,actual在后,只是一种需要学习的约定罢了。
二元函数不算恶劣,而且你当然也会编写二元函数。不过,你得小心,使用二元函数要付出代价。你应该尽量利用一些机制将其转换成一元函数。例如,可以把writeField方法写成outputStream的成员之一;从而能这样用:outputStream.writeField(name)。或者,也可以把outputStream写成当前类的成员变量,从而无需再传递它。还可以分离出类似FieldWriter的新类,在其构造器中采用outputStream,并且包含一个write方法。
3.6.4 三元函数
有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问题都会加倍体现。建议你在写三元函数前一定要想清楚。
例如,设想assertEquals有三个参数:assertEquals(message,expected,.actual)。有多少次,你读到message,.错以为它是expected呢?我就常栽在这个三元函数上。实际上,每次我看到这里,总会绕半天圈子,最后学会了忽略message参数。
另一方面,这里有个并不那么险恶的三元函数:assertEquals(l.0,amount,,.001)。虽然也要费点神,还是值得的。得到“浮点值的等值是相对而言”的提示总是好的。
3.6.5参数对象
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
例如,下面两个声明的差别:
circle makecircle(double x,double y,double radius);
Circle makecircle(Point center,double radius);
从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参数被共同传递,就像上例中的x和y那样,往往就是该有自己名称的某个概念的一部分。
3.6.6参数列表
有时,我们想要向函数传入数量可变的参数。例如,String.format方法:
String.format ("%s worked &.2f hours.",name,hours);
如果可变参数像上例中那样被同等对待,就和类型为Ls的单个参数没什么两样。这样
一来,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);
3.6.7动词与关键字
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。例如,write(name)就相当令人认同。
不管这个“name”是什么,都要被“write”。更好的名称大概是writeField(name),它告诉我们,“name”是一个“field”。
最后那个例子展示了函数名称的关键字(keyword).形式。使用这种形式,我们把参数的名称编码成了函数名。例如,assertEqual改成assertExpectedEqualsActual(expected,.actual)可能会好些。这大大减轻了记忆参数顺序的负担。
3.8分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。看看下面的例子:
public boolean set(String attribute,String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个属性则返回fals。这样就导致了以下语句:
if (set ("username","unclebob"))...
从读者的角度考虑一下吧。这是什么意思呢?它是在问username属性值是否之前已设置为unclebob吗?或者它是在问username属性值是否成功设置为unclebob呢?从这行调用很难判断其含义,因为st是动词还是形容词并不清楚。
作者本意,set是个动词,但在if语句的上下文中,感觉它像是个形容词。该语句读起来像是说“如果username属性值之前已被设置为uncleob”,而不是“设置username属性值为unclebob,看看是否可行,然后…”。要解决这个问题,可以将set函数重命名为setAndCheckIfExists,但这对提高if语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists("username")) {
(setAttribute("username","unclebob");
}
3.9使用异常替代返回错误码
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。它鼓励了在f语句判断中把指令当作表达式使用。
if (deletePage(page)==E_OK)
这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。当返回错误码时,就是在
要求调用者立刻处理错误。
另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化:
3.9.1抽离TryLatch代码块
Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一淡。最好把ry和catch代码块的主体部分抽离出来,另外形成函数。
3.9.2错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字y在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
3.11结构化编程
有些程序员遵循Edsger Dijkstra的结构化编程规则。Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益不大。只有在大函数中,这些规则才会有明显的好处。
所以,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,gto只在大函数中才有道理,所以应该尽量避免使用。
相关文章:
代码整洁之道(第3章节)--函数
目录 3.1 短小 3.2 只做一件事 3.3 每个函数一个抽象层级 3.4switch语句 3.5 使用描述性的名称 3.6 函数参数 3.6.1 一元函数的普遍形式 3.6.2标识参数 3.6.3 二元函数 3.6.4 三元函数 3.6.5参数对象 3.6.6参数列表 3.6.7动词与关键字 3.8分隔指令与询问 3.9使用…...

92. UE5 GAS RPG 使用C++创建GE实现灼烧的负面效果
在正常游戏里,有些伤害技能会携带一些负面效果,比如火焰伤害的技能会携带燃烧效果,敌人在受到伤害后,会接受一个燃烧的效果,燃烧效果会在敌人身上持续一段时间,并且持续受到火焰灼烧。 我们将在这一篇文章里…...

Echarts可视化大屏数据详解
1、ECharts介绍 1.1、什么是ECharts ECharts是一款由百度开发并开源的数据可视化图表库,旨在帮助开发者通过简单易用的方式实现复杂的数据展示和分析需求。它完全基于 JavaScript 开发,利用 HTML5 的 Canvas 技术进行图形渲染,这使得它能够…...

Linux---文件(2)---文件描述符缓冲区(语言级)
目录 文件描述符 基础知识 文件描述符 对“Linux一切皆文件”的理解 文件描述符分配规则 缓冲区 刷新策略 存放位置 解释一个"奇怪的现象" 格式化输入输出 文件描述符 基础知识 在系统层面上,文件操作都是通过文件描述符来操作的。 程序在启…...

云计算实训39——Harbor仓库的使用、Docker-compose的编排、YAML文件
一、Harbor部署 1.验证python版本 [rootdocker2 ~]#python --version 2.安装pip [rootdocker2 ~]# yum -y install python2-pip #由于版本过低,需要对其进行一个升级 #更新pip [rootdocker2 ~]#pip install --upgrade pip 3.指定版本号 [rootdocker2 ~]# p…...

lambda表达式用法——C#学习笔记
“Lambda 表达式”是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型。 实例如下: 代码如下: using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.…...

【C++ Primer Plus习题】11.6
问题: 解答: main.cpp #include <iostream> #include "Stonewt.h" using namespace std; const int SIZE 6;int main() {Stonewt stone_arr[SIZE] { 253.6,Stonewt(8,0.35),Stonewt(23,0) };double input;Stonewt eleven Stonewt(11, 0.0);Stonewt max st…...
Redis八种数据结构简介
Redis数据结构 Redis新旧版本中一共出现过八种数据结构,分别是SDS、双向链表、压缩列表、整数集合、哈希表、跳表、quicklist、listpack。 SDS SDS是用于存储Redis中字符串的数据结构,Redis底层使用的语言是C语言,因此字符串也是C语言的字…...
数据治理策略:确保数据资产的安全与高效利用
数据治理策略:确保数据资产的安全与高效利用 在数字化时代,数据已成为企业最宝贵的资产之一。然而,随着数据量的爆炸性增长和数据来源的多样化,如何有效地管理和利用这些数据成为企业面临的重要挑战。数据治理策略的制定和执行&a…...

ts格式转mp4,四款亲测好用软件推荐!
在这个数字视频时代,我们经常会遇到各种视频格式兼容性问题,尤其是从网络下载的高清电影或电视剧集,很多时候都是以TS格式存储。然而,当我们想要在移动设备、社交媒体或视频编辑软件中播放、上传时,MP4格式因其广泛的兼…...

10、Django Admin修改标题
admin from django.contrib import admin from .models import Category, Origin, Hero, Villain # 添加以下代码 admin.site.site_header "系统管理" admin.site.site_title "管理员界面" admin.site.index_title "欢迎来到这里ÿ…...

ESRI ArcGIS Pro 3.1.5新功能及安装教程和下载
ESRI ArcGIS Pro 3.1.5 主要新功能包括: 改进的数据编辑和管理:支持更多数据格式和更精细的属性表操作。增强的空间分析工具:新增和优化空间分析工具,提高数据分析效率。更好的3D可视化:改进3D渲染性能,支…...

人工智能,语音识别也算一种人工智能。
现在挺晚了,还是没有去睡觉,自己在想什么呢,也不确定。 这是一篇用语音写的文章,先按自己的想法说出来,然后再适当修改,也许就是一个不错的文章。 看来以后就不需要打字了,语音识别度很高&#…...
Token和Refresh Token
获取令牌(Token) 和 刷新令牌(Refresh Token) 在认证和授权机制中有不同的使用场景和目的,二者主要的区别和为什么需要刷新令牌可以通过以下几点解释: 1. 获取令牌和刷新令牌的区别 获取令牌(Token&#x…...

STM32(一)简介
一、stm32简介 1.外设接口 通过程序配置外设来完成功能 2.系统结构 3.引脚定义 4.启动配置 5.最小系统电路...

接口测试工具:Postman详解
🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 一、前言 在前后端分离开发时,后端工作人员完成系统接口开发后,需要与前端人员对接,测试调试接口,验证接口的正确性…...
Linux中全局变量配置,/etc/profile.d还是/etc/profile
全局环境变量可以放在 /etc/profile 或 /etc/profile.d/ 中,但两者的使用方式和目的有所不同: /etc/profile 作用: /etc/profile 是一个系统范围的启动脚本,在用户登录时执行。它主要用于设置全局环境变量和配置,适用于所有用户。…...

【java入门】关键字、标识符与变量初识
🚀 个人简介:某大型国企资深软件开发工程师,信息系统项目管理师、CSDN优质创作者、阿里云专家博主,华为云云享专家,分享前端后端相关技术与工作常见问题~ 💟 作 者:码喽的自我修养ǹ…...
Java常用类库
Java常用类库 1. **Java基础类库**1.1 **java.lang**1.2 **java.util**1.3 **java.io**1.4 **java.nio**1.5 **java.time** 2. **第三方类库**2.1 **Apache Commons**2.2 **Google Guava**2.3 **Jackson**2.4 **Lombok** 3. **Spring相关类库**4. **并发类库**4.1 **java.util.…...

2024年高教社杯数学建模国赛C题超详细解题思路分析
本次国赛预测题目难度,选题人数如下所示 难度评估 A:B:C 1.8:1.3:1 D:E1.5:1 选题人数 A:B:C 1:1.5:2.8 D:E0.5:1.2 C题一直以来都是竞赛难度最低、选题人数最多的一道本科生选题,近三年C题的选题人数一直都是总参赛队伍的一半左右,2023年…...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》
引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...

2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...

Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...