《整齐代码之道》、《编写可读代码的艺术》、《重构——改进既有代码的规划》读书笔记
布景
前菜
什么样的代码是整齐的?
衡量代码质量的唯一标准,是别人阅览你代码时的感触。所谓整齐代码,即可读性高、易于了解的代码。
不整齐的代码,阅览体会是这样的:
- 乱(组织乱、责任乱、称号乱起)
- 逻辑不明晰(if-else太多)
- 绕弯子(简略的事写的很杂乱)
- 看不明白(只要写的人能了解)
- 难修正(耦合严峻,各种写死)
整齐的代码,阅览体会是这样的:
- 明晰(是什么,做了什么,一眼看得出来)
- 简略(责任少,代码少,逻辑少)
- 干净(没有多余的逻辑)
- 好拓展(依靠的比较少,修正不会影响许多)
为什么需求编写整齐的代码?
-
坚持代码整齐是程序员专业性的重要体现。 写软件就像是盖房子,很难幻想一个地板不平、门窗关不严实的房子能称为一个大师制造。代码整齐能够体现一个人的专业水平和寻求专业性的态度。
-
读代码的时刻远远大于写代码。 依据《整齐代码之道》作者在书中小数据量统计,读代码与写代码的时刻比或许达到10:1,实际项目中尽管达不到这个比例,可是需求阅览其他同学代码的场景并不少见。让代码简略阅览和了解,能够优化阅览代码的时刻本钱和交流本钱。
-
不整齐的代码带来许多坏处。
- 每一笔不整齐的代码都是一笔技能债,早晚需求偿还,且随着时刻的推移,偿还本钱或许会越来越大。
- 烂代码难以了解,不敢改动,简略按住葫芦浮起瓢,修完一个bug又引入了另一个bug。
- 阅览不好的代码,会让人心情烦躁,充溢负能量,是一种精神折磨。
- 简略引起破窗效应。当代码开端有一些bad smell,因为破窗效应,或许会导致代码越来越烂,不断堆集构成“屎山”。
让代码变得整齐
命名
名副其实
在新建变量、函数或类的时分,给一个语义化的命名,不要因为惧怕花时刻取姓名就先随手写一个想着今后再改(个人经验今后大概率是不会再改,或许想改的时分忘记要改哪里了)。假如称号需求注释来补充,那就不算是名副其实。
防止误导
起姓名时,防止别人将这个姓名误读成其他的含义。有以下几条准则能够运用:
- 防止运用和本意相悖的词。
e.g.表达一组账号的变量:
- 假如不是一个List类型,不要运用accountList。
- 主张运用accountGroup。
- 防止有歧义的命名。
e.g. 表达过滤后剩余的数据
- 不要运用filteredUsers,filter具有二义性,不清楚究竟是被过滤的,仍是过滤后剩余的。
- 主张运用removedUsers、remainedUsers来别离标明被过滤的和过滤后剩余的。
- 防止运用外形相似度高的称号。
e.g.简略和单选:
- 不要运用simple和single,外形相似,简略混杂。
- 主张运用easy和single。
- 防止运用不常见的缩写。
防止运用没有区别性的命名
- 防止运用一些很广泛的词: 比方Product类和ProductInfo或许ProductData类称号尽管不同,但其实意思是相同的。应该运用更有区别性的命名。再比方,getSize或许回来一个数据结构的长度、所占空间等,改成getLength或getMemoryBytes则更适宜一些。
- 防止tmp之类的姓名: 除非真的是显而易见且无关紧要的变量,否则不要运用tmpXxx来命名。
- 修正 IDE 主动生成的 变量名 : IDE主动生成变量姓名,有些时分是没有语义的,为了易于了解,在生成代码后,顺便修正变量姓名。
/// BAD: element标明的语义是啥?需求结合前面的selectedOptions来揣度element的语义
List<String> get selectedKeys {
return selectedOptions.map((element) => element.key).toList();
}
/// GOOD: 阅览代码即可知道获取的是已选选项的key
List<String> get selectedKeys {
return selectedOptions.map((option) => option.key).toList();
}
给变量名带上重要的细节
- 标明衡量的 变量名 带上单位: 假如变量是一个衡量(长度、字节数),最好在姓名中带上它的单位。比方:startMs、delaySecs、sizeMb等。
- 附带其他属性: 比方未处理的变量前面加上raw。
不运用魔法数字
遇到常量时,防止直接将魔法数字编写到代码中。这种办法有许多坏处:
- 没有语义,需求考虑这个魔法数字代表什么意思。从而导致这个代码只要写的人敢改。
- 假如该魔法数呈现多次,之后修正时需求掩盖到每个运用之处,一旦有一处没改,就会有危险。
- 不便于搜索。
主张改为表达目的的常量,或运用枚举。
/// BAD: 需求消耗注意力寻找2的含义
if (status == 2) {
retry();
}
/// GOOD: 改为表达目的的命名变量
const int timeOut = 2;
if (status == timeOut) {
retry();
}
防止拼写过错
AndroidStudio有自带的拼写查看,平常在写代码的时分能够注意一下拼写过错提示。
注意变量名的长度
变量名不能太长,也不能太短。 太长的姓名读起来太费力,太短的姓名读不明白是什么意思。那变量名长度究竟多少最适宜呢?这个问题没有结论,可是在决议计划变量名长度时,有一些准则能够运用:
- 在小的效果域里能够运用短的姓名: 效果域小的标识符不用带上太多信息。
- 丢掉没用的词: 有时分姓名中的某些单词能够拿掉且不会丢失任何信息。例如:convertToString能够替换为toString。
- 运用常见的缩写下降变量长度: 例如,pre替代previous、eval替代evaluation、doc替代document、tmp替代temporary、str替代string。
e.g. 在办法里,运用tempMap命名,只需求了解它是用于暂时存储,最终作为回来值;可是假如tempMap是在一个类中,那么看到这个变量或许就会比较费解了。
static Map<String, dynamic> toMap(List<Pair> valuePairs) { Map<String, dynamic> tempMap = {}; for (final pair in valuePairs) { tempMap[pair.first] = pair.second; } return tempMap; }
附:一些常用 命名标准 。
变量
删去没有价值的暂时变量
当某个暂时变量满足以下条件时,能够删去这个暂时变量:
- 没有拆分任何杂乱的表达式。
- 没有做更多的澄清,即表达式自身就已经比较简略了解了。
- 只用过一次,并没有紧缩任何冗余代码。
/// BAD: 运用暂时变量now
final now = datetime.datetime.now();
rootMessage.lastVisitTime = now;
/// GOOD: 去除暂时变量now
rootMessage.lastVisitTime = datetime.datetime.now();
缩小变量的效果域
-
谨慎运用大局变量。 因为很难盯梢这些大局变量在哪里以及怎么运用他们,并且过多的大局变量或许会导致与局部变量命名抵触,从而使得代码会意外地改变大局变量的值。所以在界说大局变量时,问自己一个问题,它必定要被界说成大局变量吗?
-
让你的变量对尽或许少的代码可见。 因为这样有用地削减了读者需求一起考虑的变量个数,假如能把一切的变量效果于都折半,则意味着一起需求考虑的变量个数平均来说是原来的一半。比方:
- 当类中的成员变量太多时,能够将大的类拆分红小的类,让某些变量成为小类中的私有变量。
- 界说类的成员变量或办法时,假如不期望外界运用,将它界说成私有的。
-
把界说下移。 把变量的界说放在紧贴着它运用的地方。不要在函数或句子块的顶端直接放上一切需求运用的变量的界说,这会让读者在还没开端阅览代码的时分逼迫考虑这几个变量的含义,并在接下来的阅览中,不断地索引是哪个变量。
函数
-
防止长函数
- 函数要矮小!函数要矮小!函数要矮小!(重要的作业说三遍)
- 每个函数只做一件事。
假如发现一个函数太长,一般都是一个函数里干了太多作业,能够运用Extract Method(提取函数) 重构技巧,将函数拆分红若干个子功用,放到若干个子函数中,并给每个子函数一个语义化的命名(必要时能够添加注释)。这样既进步了函数的可读性,一起矮小、单一功用的函数也便利复用。
防止太重的分支逻辑
if-else句子、switch句子、try-catch句子中,假如某个分支过于杂乱,能够将该分支的内容提炼成独立的函数。 这样不但能坚持函数矮小,并且因为块内调用的函数具有较具说明性的称号,从而添加了文档上的价值。
/// BAD: if-else中句子多且繁杂
if (date.before(summerStart) || date.after(summerEnd)) {
charge = quantity * winterRate + winterServiceCharge;
} else {
charge = quantity * summerRate;
}
/// GOOD: 别离提炼函数
if (notSummer(date)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge(quantity);
}
运用具有语义化的描述性称号给函数命名
- 函数称号应具有描述性,别惧怕长的称号。长而具有描述性的称号,要比短而令人费解的称号好。
- 别惧怕花时刻取姓名。
下降参数个数
-
参数个数越少,了解起来越简略。一起也意味着单测需求掩盖的参数组合少,有利于写单测。
-
当输入参数中有bool值时,主张运用Dart中的命名参数。
/// BAD: bool类型取值只要true和false,无法了解在这个场景下取值的含义,必须得点到办法的声明里 search(true); /// GOOD: 经过命名函数能够了解到取值的含义 search(forceSearch : true);
-
当输入参数过多时,主张将其中一些参数封装成类。否则后续常常添加一个参数,就得修正函数的声明。
/// BAD: 函数参数中放置多个离散的数据项 void initUser({ required String key, required String name, required int age, required String sex, }) { ... } /// GOOD: 将紧密相连的数据项聚合到一个类中 class UserInfo { String key; String name; String sex; int age; } void initStore({required UserInfo user}) { ... }
分隔指令和查询
函数要么做什么事,要么答复什么事,二者不可得兼。函数应该修正某方针的状况,或是回来该方针的有关信息。两样都干常会导致逻辑紊乱。
注释
真正好的注释只要一种,那便是经过其他办法不写注释。
- 假如你发现自己需求写注释,再想想看能否用更明晰的代码来表达。
- 为什么要贬低注释的价值?注释存在的时刻越久,就离所描述的代码越远,变得越来越过错,因为程序员不能坚持维护注释。
- 这个方针也并非铁律,项目中经常会存在一些千奇百怪布景的代码,指望全部靠代码表达是不或许的。
坏的注释
- 臃肿的、不清楚的、令人费解的注释。 假如不确定注释写的是否适宜,让你旁边的同学看下能不能看懂。
- 简略的代码,杂乱的注释。 阅览注释比代码自身更费时。
- 有误导的注释。 随着事务迭代,在改代码的时分并没有更改对应的注释,导致代码逻辑与注释不匹配,引起误解,这对其他开发人员是致命的。
- 显而易见的东西,没必要写注释。
- 注释不需求的代码。 不需求的代码不要经过注释的办法保存,直接删掉就好。否则别人或许也不敢删去这段代码,会觉得放在那里是有原因的。
- 注释不应该用于粉饰不好的规划。 比方给函数或变量随便取了个姓名,然后花一大段注释来解说。应该想办法取个更易懂的姓名。
好的注释
以下场景值得加上注释:
- 代码中含有杂乱的事务逻辑,或需求必定的上下文才干了解的代码。 假如期望阅览者对代码布景或代码规划有个大局的了解,能够附上相关的文档链接。
- 用输入输出举例,来说明特别的状况。 相较于大段注释来说,一个精心挑选的输入、输出例子更有用。
- 将一些晦涩的参数和回来值翻译成可读的东西。
assertTrue(a.compareTo(a) == 0); // a == a
assertTrue(a.compareTo(b) != 0); // a != b
- 代码中的正告、着重,防止其他人调用代码时踩坑。 在写注释的时分问问自己:“这段代码有什么出人意料的地方?会不会被误用?”预料到其他人运用你的代码时或许会遇到的问题,再针对问题写注释。
- 对代码的想法。 TODO(待办)、FIXME(有问题的代码)、HACK(对一个问题不得不选用的比较粗糙的解决方案)或一些自界说的注释(REFACTOR、WARNING)。
- 在文件、类的级别上,运用“大局观”的注释来解说一切的部分是怎么作业的。 用注释来总结代码块,使读者不至于迷失在细节中。
- 代码注释应该仅答复代码不能答复的问题。 例如,办法注释应当应该写的是“为什么存在这个办法” 和 “办法做了什么”,而不是“办法是怎么完成的”。假如办法注释过于重视办法“怎么”作业,那么随着代码的不断改变,它很快就会过期。当开发人员依靠过期的注释来了解办法的作业原理时,这或许会导致混杂和过错。
格式
限制单个文件的代码行数
上图统计了Java中一些闻名项目的每个文件的代码行数。能够看到都是由许多行数比较小的文件构成,没有超越500行的单独文件,大多数都少于200行。
小文件一般比大文件愈加简略了解。 尽管这不是一个硬性规定,但一般一个文件不应该超越200行,且上限为500行。
dart中,能够经过part字段,对长文件进行拆分。
-
限制代码的长度
眼睛在阅览高而窄的文本时会更舒畅,这正是报纸文章看起来是这样的原因:防止编写太长的代码行是一个很好的做法。别的,短行代码在运用三栏办法解抵触的时分,不需求横向滚动,更简略发现抵触的内容。
/// BAD: 参数放在一行展现
Future navigateToXxxPage({required BuildContext context, required Map<String, dynamic> queryParams, Object? arguments,});
/// GOOD: 每个参数一行展现,更明晰
Future navigateToXxxPage({
required BuildContext context,
required Map<String, dynamic> queryParams,
Object? arguments,
});
合理运用代码中的空行
源代码中的空行能够很好的区别不同的概念。反之,内容相关的代码不应该空行,应该紧贴在一起。
变量、函数声明
- 变量的声明应尽或许接近它运用的地方。 类的成员变量的声明应该呈现在类的顶部。局部运用的变量应该声明在它运用之处邻近。
- Dart函数中参数的声明,required标记的参数尽量归在一起。
- 假如有一堆变量要声明(类的成员变量、函数的参数),能够从重要的到不重要的进行排序。
- 假如一个函数调用另一个函数,它们应该在垂直上接近,并且假如或许的话,调用者应该在被调用者之上。 在一般状况下,咱们期望函数调用依靠关系指向向下的方向。也便是说,一个被调用的函数应该在一个履行调用的函数下面。像在看报纸相同,咱们等待最重要的概念最先呈现,低层次的细节呈现在最终。
简化操控流、表达式
假如代码中没有条件判别、循环或许任何其他的操控流句子,那么它的可读性会很好。而跳转和分支等部分则会很快地让代码变得紊乱。
调整条件句子中参数的次序
比较的左值为变量,右值为常量。这种办法更符合天然语言的次序。
/// BAD:
if (10 <= length)
/// GOOD:
if (length >= 10)
调整if-else句子块的次序
在写if-else句子的时分:
- 首要处理正逻辑而不是负逻辑的状况。例如,用
if(debug)
而不是if(!debug)
。 - 先处理掉简略的状况。这种办法或许还会使得if和else在屏幕之内都可见。
- 先处理有趣的或许是可疑的状况。
兼并相同回来值
当有一系列的条件测验回来同样的成果时,能够将这些测验兼并成一个条件表达式,并将这个条件表达式提炼成一个独立函数。
/// BAD: 多个条件分开写,可是回来了同一个值。
int test() {
final bool case1 = ...;
final bool case2 = ...;
final bool case3 = ...;
if (case1) {
return 0;
}
if (case2) {
return 0;
}
if (case3) {
return 0;
}
return 1;
}
/// GOOD:将统一回来值对应的条件兼并。
int test() {
if (shouldReturnZero()) {
return 0;
}
return 1;
}
bool shouldReturnZero() {
final bool case1 = ...;
final bool case2 = ...;
final bool case3 = ...;
return case1 || case2 || case3;
}
不要寻求下降代码行数而写出难了解的表达式
三目运算符能够写出紧凑的代码,可是不要为了将一切代码都挤到一行里而运用三目运算符。三目运算符应该是从两个简略的值中做挑选,假如逻辑杂乱,运用if-else更好。
/// BAD:
return exponent >= 0 ? mantissa * (1 << exponent): mantissa/(1<<-exponent);
/// GOOD:
if (exponent >= 0) {
return mantissa * (1 << exponent);
} else {
return mantissa / (1 << -exponent);
}
防止嵌套过深
条件表达式一般有两种表现方式:
- 一切分支都属于正常行为(运用if-else方式)。
- 只要一种是正常行为,其他都是不常见的状况(if-if-if…-正常状况)。
嵌套过多深使代码更难读取和盯梢,能够尽量将代码转为以上两种标准的if方式。
/// BAD: if-else嵌套太深,难以了解逻辑。
void test() {
if (case1) {
return a;
} else {
if (case2) {
return b;
} else {
if (case3) {
return c;
} else {
return d;
}
}
}
}
/// GOOD: 先处理非正常状况,直接退出,再处理正常状况,下降了解本钱
void test() {
if (case1) return a;
if (case2) return b;
if (case3) return c;
return d;
}
不要运用if-else替代switch
一般运用switch的场景,都是某个变量有可枚举的取值,比方枚举类型,不要运用if-else来替代枚举值的判别:
enum State {success, failed, loading}
/// BAD: 关于现在的流程是没问题,可是假如新增了一个State,忘记修正这儿,就会呈现危险;
/// 何况switch自身就适合在这种场景下运用。
void fun() {
if (state == State.success) {
// do something when success
} else if (state == State.failed) {
// do something when failed
} else {
// do something when loading
}
}
/// GOOD: 当State新增了一个枚举值时,这儿会报错,必须修正这儿才干编译经过
void fun () {
switch (state) {
case State.success:
// do something when success
break;
case State.failed:
// do something when failed
break;
case State.loading:
// do something when loading
break;
}
}
让表达式更易读
日常写代码时,最常见的一个现象便是if句子的条件中,包含了很多的与或非表达式,假如表达式的逻辑简略还好,一旦表达式开端嵌套或多个与或非并排,那么关于了解代码的人来说将是一个灾难。遇到这种状况,能够运用以下的技巧,逐步优化代码:
-
提取解说变量。 引入额外的变量,来标明一个小一点的子表达式。
/// BAD: 阅览代码的人需求了解line.split(":")[0].trim()代表什么,当没有注释时往往纯靠猜想 if (line.split(":")[0].trim() == "root") { // xxx } /// GOOD: 快速了解line.split(":")[0].trim()的语义,便于了解if条件表达式 final userName = line.split(":")[0].trim(); if (userName == "root") { // xxx } /// BAD: 了解这个表达式需求花多久? if (line.split(":")[0].trim() == "root" || line.split(":")[1].trim() == "admin") { // xxx } /// GOOD: 仍是这个更简略了解? final isRootUser = line.split(":")[0].trim() == "root"; final isAdminUser = line.split(":")[1].trim() == "admin"; if (isRootUser || isAdminUser) { // xxx }
-
运用总结变量。 当if句子的条件比较杂乱时,将整个条件表达式运用一个总结变量替代。
/// BAD: 阅览代码的人需求了解什么状况下能进入if句子,代表什么语义 if (newSelect != null && preSelect != null && newSelect != preSelect) { // xxx } /// GOOD: 快速了解if句子的语义,假如重视细节,再看表达式的构成 final selectionChanged = newSelect != null && preSelect != null && newSelect != preSelect; if (selectionChanged) { // xxx }
-
削减非逻辑嵌套。 关于一个bool表达式,有一下两种等价写法,大家能够自行判别哪个愈加可读。
/// BAD: 阅览代码的人需求了解什么状况下能进入if句子,代表什么语义 if (!(fileExists && !isProtected)) { // xxx } /// GOOD: 快速了解if句子的语义,假如重视细节,再看表达式的构成 if (!fileExists || isProtected) { // xxx }
类
类应该矮小
与函数相同,在规划类时,首要规矩便是尽或许矮小。关于函数,点评的目标是代码行数;关于类,点评目标则为责任,即假如无法为某个类取个精准的称号,那就标明这个类太长了。
那么,怎么让类坚持矮小呢?这儿需求先介绍一条准则:
单一责任准则:即类或模块应有且只要一条加以修正的理由,即一个类只负责一项责任。
运用单一责任,将大类拆分为若干内聚性高的小类,即可完成类应该矮小的规矩。
- 所谓内聚,即类应该只要少量实体变量,类中的每个办法都应该操作一个或多个这种变量。一般而言,办法操作的变量越多,则内聚性越高。
- 让代码能运转和坚持代码整齐,是天壤之别的两项作业。大多数人往往把精力花在了前者,这是没问题的。问题在于,当代码能运转后,不是马上转向完成下一个功用,而是回头将臃肿的类切分红只要单一责任的去耦合单元。
- 许多开发者或许会觉得运用单一责任会导致类的数量变多,但其实这种办法会让杂乱系统中的检索和修正变得愈加明晰简略。
为修正而组织
编写代码时,需求考虑今后的修正是否便利,下降修正代码的危险。
敞开封闭准则 :类应当对扩展敞开,对修正封闭。
单元测验
- 软件界旷世之架:测验驱动开发(TDD)之争
- 测验篇——测验驱动开发(TDD)
测验驱动开发(Test-Driven Development, TDD),要求在编写某个功用的代码之前先编写测验代码,然后只编写使测验经过的功用代码,经过测验来推进整个开发的进行。 这有助于编写简练可用和高质量的代码,并加快开发过程。
尽管在日常开发中,咱们并不是按照这种办法开发,可是这种思想关于进步代码能力大有裨益。也许你会好奇,这种测验先行的办法与测验后行的办法有什么区别?总结下来便是:
- 假如你首要编写 测验用例 ,那么你将能够更早发现缺陷,一起也更简略修正它们。 当你聚集在一个办法时,将会更简略发现这个办法的鸿沟case,而咱们代码中大部分的缺陷都是因为一些鸿沟case遗漏导致的。
- 在编写代码之前先编写 测验用例 ,能更早地把需求上的问题露出出来。 在写事务代码前,先考虑测验用例有哪些,一些鸿沟问题天然就会浮出水面,迫使你去解决。
- 首要编写 测验用例 ,将迫使你在开端写代码之前至少考虑一下需求和规划,而这往往会催生更高质量的代码。 写测验,你就会站在代码用户的视点来考虑,而不仅仅是一个单纯的完成者,因为你自己要运用它们,所以能规划一个更有用,更一致的接口。别的为了确保代码的可测性,也迫使你会将不同的逻辑解耦,下降测验需求的上下文。
坚持测验整齐
-
假如没有测验代码,程序员会不敢动他的事务代码。 这点在改表单、计算引擎逻辑时深有体会,经常按下葫芦浮起瓢。
-
修正事务代码的一起,相应的测验代码也需求修正。 假如单测不能跑,那单测就毫无含义,写单测并不是为了应付,而是确保代码的正确性,所以不要因为懒得修正导致破窗效应。
-
怎么让测验代码整齐? 可读性!可读性!可读性!单元测验中的可读性比生产代码中愈加重要。测验代码中,相同的代码应抽象在一起,遵循 Build-Operate-Check 准则,即每一个测验应该明晰的由以下这三个部分组成:
- Build: 构建测验数据。
- Operation: 操作测验数据。
- Check: 检验操作是否得到期望成果。
-
每个 测验用例 只做一件事。 不要写出超长的测验函数。假如想要测验3个功用,便是拆成3个测验用例。
整齐测验规矩(F.I.R.S.T)
- 快速(Fast) :测验代码需求履行得很快。测验运转慢→不想频繁运转测验代码→不能尽早发现生产代码的问题→代码腐坏。
- 独立(Independent):测验代码不应该相互依靠,某个测验不应该成为下一个测验的设定条件。测验代码都应该能够独立运转,以及按任何次序运转。当测验相互依靠时,会导致问题难以定位。
- 可重复(Repeatable):测验代码应该在任何环境下都能够重复履行。
- 自足验证(Self-Validating) :测验需求有一个bool类型的输出。不能经过看log判别测验是否经过,而应该经过断言。
- 及时(Timely):测验代码需求及时更新,在编写事务代码之前先写测验代码。假如程序员先写事务代码,很有或许形成写测验代码不便利的问题。