前言
本篇文章是阅览郑晔教师的<<代码之丑>>后的感悟和总结,收成颇多。
正文
“写代码“有俩个维度:正确性和可保护性,把代码写对,是每一个程序员的必备技能,能够把代码写的具有可保护性,这是一个程序员从事务迈向工作的榜首步。(这句话另一位C++大佬也说过相似的:写给机器读得懂的代码不是本事,要能写出人能读得懂的代码。)
关于怎么写出可保护性的代码,有许多经典书本,比方<<程序设计实践 >>、<<代码整洁之道>>、<<重构>>等,可是都无法避免一个无情的事实:程序员们大多会认同这些书上的观点,可是每个人关于这些观点的了解却是千差万别的。
比方书上说”命名是要有意义的”,或许许多人仅仅觉得不必”abc”这种命名便是有意义,可是命名有意义远不止如此,比方代码中常用的”Info”、”Data”、”Manager”等都可能是没有意义的,都是不和代码。这些不和代码,在<<重构>>这本书中,起了一个姓名,叫做代码的坏滋味(Bad Smell)。
关于代码中的坏滋味,往往是很难发现的,因为它们不像代码Bug这么简单发现,所以郑教师就以代码中的坏滋味来说,说怎么发现代码中常见的坏滋味,以及怎么处理。
这儿有一份总结,也叫做”坏滋味自查表“,后面内容会每项单独分析,总结如下:
- 命名:
- 命名是否具有事务意义;
- 命名是否符合英语语法;
- 函数:
- 代码行是否超过__行;
- 参数列表是否超过__个;
- 类
- 类的字段是否超过__个;
- 类之间的依赖关系是否符合架构规矩;
- 句子
- 是否运用for循环;
- 是否运用else;
- 是否有重复的switch;
- 一行代码中是否了连续的办法调用;
- 代码中是否呈现了setter;
- 变量声明之后是否有当即再赋值;
- 调集声明之后是否有当即添加元素;
- 返回值是否能够运用Optional;
上面的每一个部分,后面会详细阐明为什么是坏滋味,以及怎么来修正。
命名
有一句叫做”当你开端考虑命名时,就阐明你在进阶了“,前面说了命名的最基本准则便是要有意义,那什么姿态的命名是无意义的,是坏滋味呢?下面罗列几种坏滋味的命名。
不精准的命名
看一段代码:
public void processChapter(long chapterId) {
Chapter chapter = this.repository.findByChapterId(chapterId);
if (chapter == null) {
throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]");
}
chapter.setTranslationState(TranslationState.TRANSLATING);
this.repository.save(chapter);
}
这段代码看起来没有问题,经过阅览代码,咱们知道该办法信息:办法名为processChapter即处理章节,传递进来一个章节Id,在办法内部先从repository中获取章节Chapter,然后修正其翻译状况为TRANSLATING(翻译中),最终再进行保存。
这个办法问题是出在办法名processChapter(处理章节)上,这个办法的确是在处理章节,可是这个姓名过分广泛,这儿把”章节状况设置为翻译中”叫做处理章节,那么把”章节状况设置为翻译完”、”修正章节内容”等操作是不是都能够叫做处理章节,即假如许多场景都能够叫做处理章节,那么处理章节就是一个过于广泛的姓名,没有错,可是不精准。
这是一种典型的命名坏滋味,标明来看姓名是有意义的,可是不能有效地反映这段代码的意义,有必要花时间和精力去阅览其间的详细逻辑,这也是部分代码难以阅览的本源。
相似的不精准的命名咱们常用的词有:data、info、flag、process、handle、build、maintain、manage、modify等,这些过于广泛的姓名许多时候都是因为在写代码的时候没有想好,就开端写代码了。
那怎么修正呢?首要,命名要能描绘这段代码在做的工作,比方前面代码做的工作是把”将章节改成翻译中”,那办法名是否应该叫做changeChapterToTranslating呢?
不可否认,比较于processChapter,这个姓名的确有前进,可是它还不算是一个好姓名,因为它更多是在描绘这段代码在做的细节。咱们之所以要将一段代码封装起来,一个重要原因便是咱们不想知道那么多细节,假如把细节平铺开来,那本质上和直接阅览代码细节差别并不大。
所以,一个好的姓名应该描绘目的,而非细节。
就这段代码而言,为什么要把翻译状况设置为翻译中,了解了事务后,这是因为在这儿开启了一个翻译的进程,所以这段代码更应该命名为startTranslation:
public void startTranslation(long chapterId) {
Chapter chapter = this.repository.findByChapterId(chapterId);
if (chapter == null) {
throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]");
}
chapter.setTranslationState(TranslationState.TRANSLATING);
this.repository.save(chapter);
}
用技能术语命名
咱们接着来看一段代码:
List<Book> bookList = service.getBooks();
能够说这是一段常见得不能再常见的代码了,可是这段代码却躲藏了一个典型问题:用技能术语命名。
这个bookList变量之所以叫做bookList,原因是它声明的类型是List,这种命名随处可见,比方xxxMap、xxxSet等。
这是一种不费脑子的命名方式,可是这种命名会带来许多问题,因为它是一种基于完结细节的命名方式。
编程有一个重要的准则是面向接口编程,这个准则从另一个视点了解,便是不要面向完结编程,因为接口是安稳的,而完结是易变的。大多数人了解是,这个准则是针对类型的,可是在命名上,也应该遵从这个准则。
为什么呢?比方我发现现在需求的是一个不重复的著作集和,也便是说这个变量的类型从List改成Set,变量类型很简单改,可是所有的变量名都能保证都改好吗?假如漏了一个,就会呈现一个叫做bookList的变量,它的类型是一个Set。
和前面相同,咱们需求一个更面向目的的姓名,比方这段代码咱们便是想拿到一堆书,所以直接命名为books:
List<Book> books = service.getBooks();
这是发现,这种更表意的姓名,是一个更有效的姓名。
还有一种,事实上,在实际的代码中,技能称号的呈现,往往就代表着它短少了一个运用的模型。
比方,在事务代码中直接呈现了Redis这种技能名词,就阐明短少一个中间层来充当模型,比方下面代码:
public Book getByIsbn(String isbn) {
Book cachedBook = redisBookStore.get(isbn);
...
return book;
}
这是一段事务代码,可是呈现了Redis,通常来说,事务需求是从缓存获取一个数据,而Redis则是一种完结罢了,咱们能够添加一个模型cache,如下:
public Book getByIsbn(String isbn) {
Book cachedBook = cache.get(isbn);
...
return book;
}
这种情况下,至于详细缓存是怎么完结的,在事务层咱们不必重视。
程序员之所以喜欢用技能称号去命名,一个重要原因是在学习写代码时,很大程序参阅了他人代码,而职业优异的代码往往是一些开源项目,在一个技能类的项目中,这些技能术语便是它的事务言语,可是关于事务项目,这个说法就有必要重新审视了。
用事务言语写代码
不论是不精准的命名,仍是技能称号命名,归根结底是一个问题:对事务了解不到位。
想编写可保护的代码,有必要要运用事务言语。从团队的视点看,让每个人根据自己的了解来命名,的确会呈现千奇百怪的姓名,所以一个良好的团队实践是树立团队的词汇表,让团队成员有信息能够参阅。
关于事务言语了解导致的坏滋味,就比较难以发现,比方下面办法声明:
public void approveChapter(long chapterId, long userId) {
...
}
这个办法的目的是确认章节内容审阅经过,这儿有一个问题,chapterId是审阅章节的ID,这个没问题,可是这个userId是什么呢?经过了解背景,咱们知道,这个userId是审阅人的userId,因为在审阅时需求记载审阅人信息。
经过事务分析,咱们会发现这个userId并不是一个好的命名,因为需求更多的了解才知道这个命名的意义,所以这儿更好的命名是审阅人的Userid,即能够修正为reviewerUserId:
public void approveChapter(long chapterId, long reviewerUserId) {
...
}
这种有必要了解事务才干发现的坏滋味,需求咱们在写代码时,明晰地明白事务流程,这样不仅能够消除坏滋味,还能够写出更简单保护地代码。
末节
坏滋味:缺乏事务意义的命名。
过错命名:
- 广泛的命名。
- 用技能术语命名。
命名遵从的准则:
- 描绘目的,而非细节。
- 面向接口编程,接口是安稳的,完结是易变的。
- 命名呈现技能称号,往往是短少一个模型。
- 运用事务言语。
乱用英语
英语是程序员绕不开的一个槛,抛去本篇文章的主题,英语也是广大程序员有必要要去学习的,因为只要真实去阅览英语文档或许一些API的英语注释,才干保证最高保真了解其意义,而不是直接查看经过翻译软件翻译出来的可能失真的文档。
继续本篇文章的主题,现在干流的言语都是以英语为基础的,所以想成为一个优异的程序员,有必要要会用英语。这儿不要求程序员的英语有多好,但最低限度的要求是写出来的代码要像是在用英语表达。
关于拼音和中文编程,现在的言语都是支撑的,这些一眼就能看出的坏滋味,本篇文章不做评论,咱们评论几种不易发现的坏滋味。
违背语法规矩的命名
仍是看一段代码:
public void completedTranslate(final List<ChapterId> chapterIds) {
List<Chapter> chapters = repository.findByChapterIdIn(chapterIds);
chapters.forEach(Chapter::completedTranslate);
repository.saveAll(chapters);
}
这段代码看起来没啥问题,便是把一些章节的信息标记为翻译完结,可是仔细发现这儿的办法名为completedTranslate,或许作者想表达”完结翻译”这个目的,所以完结还用了完结时的completed,翻译运用translate,可是这个姓名仍是起错了。
一般来说,常见的命名规矩是:类名是一个名词,表明一个目标,而办法名则是一个动词,或许是动宾短语,表明一个动作。
咱们以这个为标准,completedTranslate就不是一个合格的动宾结构,这儿咱们只需求把完结改成complete,翻译改成称号方式即translation即可,所以修正后办法名为completeTranslation:
public void completeTranslation(final List<ChapterId> chapterIds) {
List<Chapter> chapters = repository.findByChapterIdIn(chapterIds);
chapters.forEach(Chapter::completeTranslation);
repository.saveAll(chapters);
}
这不是一个难以察觉的坏滋味,而且常常在咱们代码里呈现,这儿需求上面的命名规矩即可。
不精确的英语词汇
比方下面代码,界说了枚举,来标识章节的审阅状况:
public enum ChapterAuditStatus {
PENDING,
APPROVED,
REJECTED;
}
这儿的命名或许看不出问题,因为把”审阅”这个单词放入翻译查询,的确会得到audit这个单词,可是同样是审阅,在其他当地却有运用review这个单词,这就造成了不一致的问题。
造成这个问题的原因是,直接在翻译软件上,”审阅”翻译为audit和review都有,而因为中英文的表达差异也没有太多人去了解。这儿想统一,就需求真实结合事务去了解每个单词真实的偏要点,经过搜索能够发现audit有更官方的滋味,相似翻译是审计,而review则更多是审查的意思,所以review更适宜,上面代码就改成了:
public enum ChapterReviewStatus {
PENDING,
APPROVED,
REJECTED;
}
比较之下,这种坏滋味便是一种高级的坏滋味,英语单词用的不精确的确是我国程序员的一个短板。在这种情况下,最好的处理方案是树立起一个事务词汇表,千万不要臆想。
树立事务词汇表的关键点便是用团体智慧,而非个体智慧,找到事务适宜的通用言语,比方上面触及章节审阅的相关词汇,能够总结为词汇表:
称号 | 英文 | 阐明 |
---|---|---|
著作 | Book | 作者创造的著作 |
章节 | Chapter | 著作的一部分 |
审阅 | review | 审阅方对著作内容进行查看的进程 |
审阅经过 | approve | 审阅方对著作内容给予经过 |
审阅未经过 | reject | 审阅方对著作内容给予不经过 |
有了这种事务词汇表,今后小组内的程序员再也不必纠结命名了。
英语单词的拼写过错
一般来说,英语单词的拼写过错在IDE中都会有提示,可是这儿有必要要留意一种特殊情况,便是把一个单词拼过错成了另一个单词。
比方下面代码:
public class QuerySort {
private final SortBy sortBy;
private final SortFiled sortFiled;
...
}
这儿的sortFiled本来是想表达”排序的字段”意思,可是把Filed拼写成了Filed,就简单让人了解为”排序文件”这种利诱的意思,所以单词拼写过错必定要留意。
关于英语才能的进步,无他,只要强迫自己操练和总结,把常见的单词回忆,多看看优异的开源项目,比较于彻底把握英语,把握编程中触及的英语仍是要简单点。
末节
本末节没有评论那种拼音命名、不恰当的命名(多个单词首字母)等这种低级的坏滋味,而是评论了几种不易发现的坏滋味。
坏滋味:乱用英语
英语运用不当:
- 违背语法规矩;
- 不精确的英语词汇;
- 英语单词拼写过错。
处理办法:
- 制定代码标准;
- 树立团队词汇表;
- 常常性进行代码评定。
去除重复
重复是一个泥潭,关于程序员来说,要时间提示自己不要重复是至关重要的。在软件开发里,有一个重要的准则叫做 Don’t Repeat Yourself(不要重复自己,简称DRY),更经典的叙说在<<程序员修炼之道>>中:在一个体系中,每一处常识都有必要有单一、明确、威望地表述。
所以本末节来看看常见的重复代码有哪些。
重复的结构
话不多说,仍是来看几段代码:
@Task
public void sendBook() {
try {
this.service.sendBook();
} catch (Throwable t) {
this.notification.send(new SendFailure(t)));
throw t;
}
}
@Task
public void sendChapter() {
try {
this.service.sendChapter();
} catch (Throwable t) {
this.notification.send(new SendFailure(t)));
throw t;
}
}
@Task
public void startTranslation() {
try {
this.service.startTranslation();
} catch (Throwable t) {
this.notification.send(new SendFailure(t)));
throw t;
}
}
这是3个事务函数,便是发送著作、发送章节和开端翻译,看起来这3个函数已经写的十分简洁了,可是这3段代码在结构上却有重复的,便是其间的catch句子。
先了解一下事务,这儿进行catch的目的便是为了防止体系出了问题无人发掘,所以这儿经过notification给飞书发送一个告诉;比较于原来的逻辑,这个逻辑是后来加上的,所以代码作者不厌其烦的在每一处都添加了这行代码。
尽管这3个函数调用的事务代码不同,可是结构是一致的,有重复的结构,咱们能够把其间重复的结构即捕获异常部分给提取出来:
private void executeTask(final Runnable runnable) {
try {
runnable.run();
} catch (Throwable t) {
this.notification.send(new SendFailure(t)));
throw t;
}
}
有了这个结构,前面几个函数就能够用来改写了,关于支撑函数式编程的程序言语来说,能够用言语供给的便当写法来简化代码,如下:
@Task
public void sendBook() {
executeTask(this.service::sendBook);
}
@Task
public void sendChapter() {
executeTask(this.service::sendChapter);
}
@Task
public void startTranslation() {
executeTask(this.service::startTranslation);
}
经过这个比方的改写,假如再有一些通用结构的调整,比方出错了需求加一些日志,就更好处理了。
这个比方十分简单,可是有2点需求留意:发现结构重复和思想改变。
关于结构重复,一般是因为事务改变,或许程序员为了便利直接复制粘贴现有的办法代码,没有做过多考虑而导致的。
关于思想改变,我为什么要这么说呢?在上一节中,咱们说命名时,类一般是称号,而函数是动词或许动宾结构,而关于参数一般是名词,这关于Java 8之前是彻底没有问题的。
可是跟着函数式编程的鼓起,比方Kotlin和Java 8支撑lambda,都是以函数为主角,即高阶函数能够作为参数。而函数是啥,函数是动作,这儿的参数就不仅仅是名词,也能够是动词了。
比方上面的比方中,修正后的代码,便是传递动作这一模范,所以跟着编程言语的前进,咱们的编程思想也需求改变和进步。
做真实的选择
仍是先来看一段代码:
if (user.isEditor()) {
service.editChapter(chapterId, title, content, true);
} else {
service.editChapter(chapterId, title, content, false);
}
这儿的逻辑是这样的:editChapter是用来修改章节的,最终一个参数表明是否审阅经过,而user可能是作者也可能是修改,当是修改时,自动审阅经过,当是作者时则默许是审阅不经过(因为无权审阅)。
初看这段代码,感觉没啥缺点,这儿也便是运用if来分开了2个不同的事务处理流程,可是仔细调查后,咱们发现2个分支调用的函数仅仅是最终一个参数不相同。
这也是一种重复代码,造成这个原因的是作者在写这段代码时,脑子只想到if句子判别之后要做什么,而没有想到这个if句子判别的到底是什么。
写代码要有表达性,要能精确地把目的表达出来,是写代码进程中十分重要的一环。明显,这儿的if判别是为了区分参数,而不是动作,所以咱们能够略微调整一下:
boolean approved = user.isEditor();
service.editChapter(chapterId, title, content, approved);
留意这儿的重视点,并没有直接把user.isEditor()当成参数传递给editChapter函数,原因是关于editChapter函数来说,最终一个Boolean类型的参数表明的是:是否审阅经过,而不是这个用户是不是修改。
这儿单独用一个approved变量,就更简单读懂代码。
末节
发现和去除重复结构,在咱们写代码中至关重要,而最难的便是应战咱们编程习惯,能够发现重复。发现重复,一种是咱们已经在泥潭中挣扎了许久才后知后觉需求修正重复代码;一种是提高自己的辨认才能,能自动发现重复。
而这种自动发现的才能,其实就需求对软件设计有更好的了解,特别是别离重视点的了解,这也是本篇文章一向贯穿的主题。
坏滋味:重复代码
重复的代码:
- 复制粘贴的代码。
- 结构重复的代码。
- if和else代码块中的句子高度相似。
消灭重复代码准则:
- 每一处常识都有必要有单一、明确、威望地描绘。
需求留意:
- 改变思想,结构重复也是重复代码,要创新地看待高阶函数运用以及动作的重复。
长函数
说起长函数,这关于程序员来说就再也了解不过了,不论是杂乱的事务代码,仍是一些源代码,总有一些几百甚至上千行的长函数。
每当在长长的函数体中找到问题所在,再小心谨慎的改动代码,都是一些不愉悦的回忆。
这个点的问题,咱们都知道,可是在说长函数之前,咱们需求知道一个点:便是多长的函数才算长?
多长的函数才算长
为什么要评论这个呢?不同的开发团队和不同的开发言语关于长函数的容忍度是不相同的,比方团队以为100行才算长函数,低于100行的不算,那绝大多数的函数都没有必要进行优化。
关于函数长度容忍度高,这是导致长函数发生的关键点。
一个好的程序员面临代码库时要有不同尺度的调查才能,看设计时,要能够建瓴高屋,看代码时,要能细致入微。
而关于长函数的长界说也是相同的,回到工作中,”越小越好”是一个追求的目标,只要有一个严厉的标准,对代码长度容忍度降低,才会供给对细节的感知力,从而发现原本所谓细枝末节的当地躲藏的问题。
所以关于Java言语,这儿建议长函数的界说是20行,当然不是一个强制标准,当然是越短越好,当一些事务无法拆分时,偶然超过20行也是能够的。
长函数的发生
这儿咱们要知道长函数发生的一些原因,假如不了解长函数发生的原因,就很难在咱们自己写代码时时间提示自己,下面罗列几个长函数发生的原因。
以功用为由
咱们都知道,函数调用的进程其实是一个入栈出栈的进程,当函数越多时,入栈出栈的次数就越多,这样会导致功用下降。
可是跟着硬件的开展,和编译器、言语自身的优化,功用优化不应该是写代码的榜首考量,更不应该拿这一点来写出长函数。
平淡无奇
在文章前面咱们也说过,界说函数便是为了把一类动作封装,让人能够一眼看出函数目的,可是许多函数完结选用平淡无奇的方式,洋洋洒洒写了几百行,特别关于杂乱的流程,这样就简单发生长函数。
关于平淡无奇的代码风格,会有2个典型问题:
- 把多个事务处理流程放在一个函数里完结;
- 把不同层次的细节放到一个函数里完结。
这2个问题就会导致代码逻辑分层紊乱,不符合单一职责准则。
而处理长函数的办法便是提取函数,把一个大函数拆分为若干个小函数。在拆分进程中,时间铭记”别离重视点”这个准则,把不同的流程、不同的层次的代码给别离开来。
关于提取后的函数,还有一个特色,便是函数名更简单取名和了解。比方许多长函数,逻辑十分多,只能用handleXXX来表明,或许用一个十分长的函数名来标明其目的,这在上一节中咱们说过,这是一种坏滋味。当长函数不存在时,咱们也更简单起名,更简单了解函数。
一次加一点
这个场景就更常见了,咱们难免会保护一些旧代码,在旧代码上新增功用,为了最小改动,咱们常常只在需求的当地加一点点代码,比方最开端的代码如下:
if (code == 400 || code == 401) {
// 做一些过错处理
}
跟着后面需求越来越多,铢积寸累,每一次就改一点,可能会变成:
if (code == 400 || code == 401 || code == 402 || ...
|| code == 500 || ...
|| ...
|| code == 10000 || ...) {
// 做一些过错处理
}
这时再去阅览,就发现很难了解了。
任何代码都经不起这种无意识的积累,每个人都没有错,可是成果很糟糕。
关于这种问题该怎么做呢?总不能不添加需求吧,这儿有一个”童子军军规”:让营地比你来时更洁净。
在编程领域也是这样,假如咱们自己对代码的改动让原有代码变得更糟糕,就需求去改善它。而这一切的条件是,自己要能发现是否会变得糟糕,所以辨认代码坏滋味十分必要。
末节
坏滋味:长函数。
长函数的发生:
- 以功用为由;
- 以平淡无奇方式写代码;
- 一次添加一点代码;
消灭长函数的准则:
- 界说好长函数的标准。
- 做好”别离重视点”,拆分长函数。
- 改代码时,据守”童子军军规”。
总结
本篇读书笔记就先到这儿,最终一点感悟,便是不论是写什么代码,都时间要记住把代码写好,要有工匠精神,编程是一门终身学习和专研的技能。
最终强烈建议阅览源文章和几本文章中提及的书本,感兴趣的能够阅览源文章:time.geekbang.org/column/intr…