你真的会给变量命名吗

举报
JavaEdge 发表于 2021/09/21 22:19:04 2021/09/21
【摘要】 有读者看到标题就开始敲键盘了,我知道,命名不就是不能用 abc、123 命名,名字要有意义嘛,这有什么好讲的? 然而,即便懂得了名字要有意义,很多程序员依然无法逃离命名沼泽。 不精准的命名 什么叫精准...

有读者看到标题就开始敲键盘了,我知道,命名不就是不能用 abc、123 命名,名字要有意义嘛,这有什么好讲的?
然而,即便懂得了名字要有意义,很多程序员依然无法逃离命名沼泽。

不精准的命名

什么叫精准?
废话不多说,CR 一段代码:

public void processChapter(long chapterId) {
  Chapter chapter = this.repository.findByChapterId(chapterId);
  if (chapter == null) {
    throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]");
t    
  }
  
  chapter.setTranslationState(TranslationState.TRANSLATING);
  this.repository.save(chapter);
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

看上去挺正常。
但我问你,这段代码在干嘛?你就需要调动全部注意力,去认真阅读这段代码,找出其中逻辑。经过阅读发现,这段代码做的就是把一个章节的翻译状态改成翻译中。

为什么你需要阅读这段代码细节,才知道这段代码在干嘛?

问题就在函数名,processChapter,这个函数确实是在处理章节,但这个名字太宽泛。如果说“将章节的翻译状态改成翻译中”叫做处理章节,那么:

  • “将章节的翻译状态改成翻译完”
  • “修改章节内容”

是不是也能叫处理章节?
所以,如果各种场景都能叫处理章节,那么处理章节就是个宽泛名,没有错,但不精准

表面看,这个名字是有含义,但实际上,并不能有效反映这段代码含义。
如果我在做的是一个信息处理系统,你根本无法判断,是一个电商平台,还是一个图书管理系统,从沟通的角度看,这就不是一个有效的沟通。要想理解它,你需要消耗大量认知成本,无论是时间,还是精力。

命名过于宽泛,不能精准描述,这是很多代码在命名上存在的严重问题,也是代码难以理解的根源所在。

或许这么说你的印象还是不深刻,看看下面这些词是不是经常出现在你的代码里:data、info、flag、process、handle、build、maintain、manage、modify 等等。这些名字都属于典型的过宽泛名字,当这些名字出现在你的代码里,多半是写代码的人当时没有想好用什么名字,就开始写代码了。

回到前面那段代码上,如果它不叫“处理章节”,那应该叫什么?首先,命名要能够描述出这段代码在做的事情。这段代码在做的事情就是“将章节修改为翻译中”。那是不是它就应该叫 changeChapterToTranlsating呢?

相比于“处理章节”,changeChapterToTranlsating这个名字已经进了一步,然而,它也不算是一个好名字,因为它更多的是在描述这段代码在做的细节。
之所以要将一段代码封装起来,是我们不想知道那么多细节。如果把细节平铺开来,那本质上和直接阅读代码细节差别不大。

所以,一个好的名字应该描述意图,而非细节。

就这段代码而言, 我们为什么要把翻译状态修改成翻译中,这一定是有原因,也就是意图
我们把翻译状态修改成翻译中,是因为我们在这里开启了一个翻译的过程。所以,这段函数应该命名 startTranslation。

public void startTranslation(long chapterId) {
  Chapter chapter = this.repository.findByChapterId(chapterId);
  if (chapter == null) {
    throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]");
t    
  }
  
  chapter.setTranslationState(TranslationState.TRANSLATING);
  this.repository.save(chapter);
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

用技术术语命名

我们再来看一段代码:

List<Book> bookList = service.getBooks();

  
 
  • 1

常见得不能再常见的代码,但却隐藏另外一个典型得不能再典型的问题:用技术术语命名。

这个 bookList 变量之所以叫 bookList,原因就是它声明的类型是 List。这种命名在代码中几乎是随处可见的,比如 xxxMap、xxxSet。

这是一种不费脑子的命名方式,但这种命名却会带来很多问题,因为它是一种基于实现细节的命名方式。

面向接口编程,从另外一个角度理解,就是不要面向实现编程,因为接口是稳定的,而实现易变。虽然在大多数人的理解里,这个原则是针对类型的,但在命名上,我们也应该遵循同样的原则。为什么?我举个例子你就知道了。

比如,如果我发现,我现在需要的是一个不重复的作品集合,也就是说,我需要把这个变量的类型从 List 改成 Set。变量类型你一定会改,但变量名你会改吗?这还真不一定,一旦出现遗忘,就会出现一个奇特的现象,一个叫 bookList 的变量,它的类型是一个 Set。这样,一个新的混淆产生了。

有什么更好的名字吗?我们需要一个更面向意图的名字。其实,我们在这段代码里真正要表达的是拿到了一堆书,所以,这个名字可以命名成 books。
List books = service.getBooks();
这个名字其实更简单,但从表意的程度上来说,它却是一个更有效的名字。

虽然这里我们只是以变量为例说明了以技术术语命名存在的问题,事实上,在实际的代码中,技术名词的出现,往往就代表着它缺少了一个应有的模型。

比如,在业务代码里如果直接出现了 Redis:

public Book getByIsbn(String isbn) {
  Book cachedBook = redisBookStore.get(isbn);
  if (cachedBook != null) {
    return cachedBook;
  }
  
  Book book = doGetByIsbn(isbn);
  redisBookStore.put(isbn, book);
  return book;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

通常来说,这里真正需要的是一个缓存。Redis 是缓存这个模型的一个实现:

public Book getByIsbn(String isbn) {
  Book cachedBook = cache.get(isbn);
  if (cachedBook != null) {
    return cachedBook;
  }
  
  Book book = doGetByIsbn(isbn);
  cache.put(isbn, book);
  return book;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

再进一步,缓存这个概念其实也是一个技术术语,从某种意义上说,它也不应该出现在业务代码。
这方面做得比较好的是 Spring。使用 Spring 框架时,如果需要缓存,我们通常是加上一个 Annotation(注解):

@Cacheable("books")
public Book getByIsbn(String isbn) {
  ...
}

  
 
  • 1
  • 2
  • 3
  • 4

之所以喜欢用技术名词去命名,一方面是因为,这是习惯的语言,另一方面也是因为学写代码,很大程度上是参考别人代码,而行业里面优秀的代码常常是一些开源项目,而这些开源项目往往是技术类项目。在一个技术类的项目中,这些技术术语其实就是它的业务语言。但对于业务项目,这个说法就必须重新审视了。

如果这个部分的代码确实就是处理一些技术,使用技术术语无可厚非,但如果是在处理业务,就要尽可能把技术术语隔离开来。

  • xxxMap这种命名表示映射关系,比如:书id与书的映射关系,不能命名为bookIdMap么?
    Map 表示的是一个数据结构,而映射关系我会写成 Mapping

用业务语言写代码

无论是不精准的命名也好,技术名词也罢,归根结底,体现的是同一个问题:对业务理解不到位。

编写可维护的代码要使用业务语言。怎么才知道自己的命名是否用的是业务语言呢?
把这个词讲给产品经理,看他知不知道是怎么回事。

从团队的角度看,让每个人根据自己的理解来命名,确实就有可能出现千奇百怪的名字,所以,一个良好的团队实践是,建立团队的词汇表,让团队成员有信息可以参考。

团队对于业务有了共同理解,我们也许就可以发现一些更高级的坏味道,比如说下面这个函数声明:

public void approveChapter(long chapterId, long userId) {
  ...
}

  
 
  • 1
  • 2
  • 3

确认章节内容审核通过。这里有一个问题,chapterId 是审核章节的 ID,这个没问题,但 userId 是什么呢?了解了一下背景,我们才知道,之所以这里要有一个 userId,是因为这里需要记录一下审核人的信息,这个 userId 就是审核人的 userId。

你看,通过业务的分析,我们会发现,这个 userId 并不是一个好的命名,因为它还需要更多的解释,更好的命名是 reviewerUserId,之所以起这个名字,因为这个用户在这个场景下扮演的角色是审核人(Reviewer)。

public void approveChapter(long chapterId, long reviewerUserId) {
  ...
}

  
 
  • 1
  • 2
  • 3

这个坏味道也是一种不精准的命名,但它不是那种一眼可见的坏味道,而是需要在业务层面上再进行讨论,所以,它是一种更高级的坏味道。

能够意识到自己的命名有问题,是程序员进阶的第一步。

@GetMapping("getTotalSettlementInfoByYear")
@ApiOperation("公司结算信息按年求和")
public Result<List<RepMonthCompanyDTO>> getTotalSettlementInfoByYear(@RequestParam String year) {
List<RepMonthCompanyDTO> list = repMonthCompanyService.getTotalSettlementInfoByYear(year);
return new Result<List<RepMonthCompanyDTO>>().ok(list);
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

名字长不是问题,问题是表达是否清晰,像repMonthCompanyService这个名字,是不太容易一眼看出来含义的。

另外,传给 service 的参数是一个字符串,这个从逻辑上是有问题的,没有进行参数的校验。后面的内容也会讲到,这个做法是一种缺乏封装的表现。

变量名是 list,按照这一讲的说法是用技术术语在命名。

再有,这个 URI 是 getTotalSettlementInfoByYear,这是不符合 REST 的命名规范的,比如,动词不应该出现在 URI 里,分词应该是“-”,byYear 实际上是一个过滤条件等等。

总结

两个典型的命名坏味道:

不精准的命名;
用技术术语命名。

命名是软件开发中两件难事之一(另一个难事是缓存失效),不好的命名本质上是增加我们的认知成本,同样也增加了后来人(包括我们自己)维护代码的成本。

  • 好的命名要体现出这段代码在做的事情,而无需展开代码了解其中的细节
  • 再进一步,好的命名要准确地体现意图,而不是实现细节
  • 更高的要求是,用业务语言写代码

好的命名,是体现业务含义的命名。

文章来源: javaedge.blog.csdn.net,作者:JavaEdge.,版权归原作者所有,如需转载,请联系作者。

原文链接:javaedge.blog.csdn.net/article/details/120398155

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。