前言

本篇文章是阅览郑晔教师的<<代码之丑>>后的感悟和总结,收成颇多。

正文

写代码“有俩个维度:正确性和可保护性,把代码写对,是每一个程序员的必备技能,能够把代码写的具有可保护性,这是一个程序员从事务迈向工作的榜首步。(这句话另一位C++大佬也说过相似的:写给机器读得懂的代码不是本事,要能写出人能读得懂的代码。)

关于怎么写出可保护性的代码,有许多经典书本,比方<<程序设计实践 >>、<<代码整洁之道>>、<<重构>>等,可是都无法避免一个无情的事实:程序员们大多会认同这些书上的观点,可是每个人关于这些观点的了解却是千差万别的

比方书上说”命名是要有意义的”,或许许多人仅仅觉得不必”abc”这种命名便是有意义,可是命名有意义远不止如此,比方代码中常用的”Info”、”Data”、”Manager”等都可能是没有意义的,都是不和代码。这些不和代码,在<<重构>>这本书中,起了一个姓名,叫做代码的坏滋味(Bad Smell)

关于代码中的坏滋味,往往是很难发现的,因为它们不像代码Bug这么简单发现,所以郑教师就以代码中的坏滋味来说,说怎么发现代码中常见的坏滋味,以及怎么处理。

这儿有一份总结,也叫做”坏滋味自查表“,后面内容会每项单独分析,总结如下:

  • 命名:
  1. 命名是否具有事务意义;
  2. 命名是否符合英语语法;
  • 函数:
  1. 代码行是否超过__行;
  2. 参数列表是否超过__个;
  1. 类的字段是否超过__个;
  2. 类之间的依赖关系是否符合架构规矩;
  • 句子
  1. 是否运用for循环;
  2. 是否运用else;
  3. 是否有重复的switch;
  4. 一行代码中是否了连续的办法调用;
  5. 代码中是否呈现了setter;
  6. 变量声明之后是否有当即再赋值;
  7. 调集声明之后是否有当即添加元素;
  8. 返回值是否能够运用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个典型问题:

  1. 把多个事务处理流程放在一个函数里完结;
  2. 把不同层次的细节放到一个函数里完结。

这2个问题就会导致代码逻辑分层紊乱,不符合单一职责准则。

而处理长函数的办法便是提取函数,把一个大函数拆分为若干个小函数。在拆分进程中,时间铭记”别离重视点”这个准则,把不同的流程、不同的层次的代码给别离开来。

关于提取后的函数,还有一个特色,便是函数名更简单取名和了解。比方许多长函数,逻辑十分多,只能用handleXXX来表明,或许用一个十分长的函数名来标明其目的,这在上一节中咱们说过,这是一种坏滋味。当长函数不存在时,咱们也更简单起名,更简单了解函数。

一次加一点

这个场景就更常见了,咱们难免会保护一些旧代码,在旧代码上新增功用,为了最小改动,咱们常常只在需求的当地加一点点代码,比方最开端的代码如下:

if (code == 400 || code == 401) {
  // 做一些过错处理
}

跟着后面需求越来越多,铢积寸累,每一次就改一点,可能会变成:

if (code == 400 || code == 401 || code == 402 || ...
  || code == 500 || ...
  || ...
  || code == 10000 || ...) {
  // 做一些过错处理
}

这时再去阅览,就发现很难了解了。

任何代码都经不起这种无意识的积累,每个人都没有错,可是成果很糟糕

关于这种问题该怎么做呢?总不能不添加需求吧,这儿有一个”童子军军规”:让营地比你来时更洁净

在编程领域也是这样,假如咱们自己对代码的改动让原有代码变得更糟糕,就需求去改善它。而这一切的条件是,自己要能发现是否会变得糟糕,所以辨认代码坏滋味十分必要。

末节

坏滋味:长函数。

长函数的发生:

  • 以功用为由;
  • 以平淡无奇方式写代码;
  • 一次添加一点代码;

消灭长函数的准则:

  • 界说好长函数的标准。
  • 做好”别离重视点”,拆分长函数。
  • 改代码时,据守”童子军军规”。

总结

本篇读书笔记就先到这儿,最终一点感悟,便是不论是写什么代码,都时间要记住把代码写好,要有工匠精神,编程是一门终身学习和专研的技能。

最终强烈建议阅览源文章和几本文章中提及的书本,感兴趣的能够阅览源文章:time.geekbang.org/column/intr…