每天进步一点点 - 坏味道方法论总结
type: channel
link: https://www.petertechblog.cn/category/%E5%9D%8F%E5%91%B3%E9%81%93%E6%96%B9%E6%B3%95%E8%AE%BA%E6%80%BB%E7%BB%93/
items: 10、 变量声明与赋值分离：普通的变量声明，怎么也有坏味道？
items: 9、 可变的数据：不要让你的代码“失控”
items: 8、 缺乏封装：如何应对火车代码和基本类型偏执问题？
items: 7、 滥用控制语句：出现控制结构，多半是错误的提示
items: 6、长参数列表：如何处理不同类型的长参数？
items: 5.大类：如何避免写出难以理解的大类？
items: 3.重复代码：简单需求到处修改，怎么办？
items: 2.如何正确的使用英文命名
items: 4 长函数：为什么你总是不可避免地写出长函数？
items: 1. 缺乏业务含义的命名：如何精准命名

10、 变量声明与赋值分离：普通的变量声明，怎么也有坏味道？
type: item
date: 2024-11-21T15:38:00+08:00
description: 变量初始化最好一次性完成。 用声明式的标准来看代码，是一个发现代码坏味道的重要参考一、初始化和业务掺杂在一起EpubStatus status = null; CreateEpubResponse response = createEpub(request); if (response.getCode() == 201) { status = EpubStatus.CREATED; } else { status = EpubStatus.TO_CREATE; }这段代码在做的事情是向另外一个服务发请求创建 EPUB（一种电子书格式），如果创建成功，返回值是 HTTP 的 201，也就表示创建成功，然后就把状态置为 CREATED；而如果没有成功，则把状态置为 TO\_CREATE。后面对于 TO\_CREATE 状态的作品，还需要再次尝试创建。这种代码真正的问题就是不清晰，变量初始化与业务处理混在在一起。解决方法：我们提取出了一个函数，将 response 转成对应的内部的 EPUB 状态。const CreateEpubResponse response = createEpub(request); const EpubStatus status = toEpubStatus(response); private: EpubStatus toEpubStatus(const CreateEpubResponse response) { if (response.getCode() == 201) { return EpubStatus.CREATED; } return EpubStatus.TO_CREATE; }在能够使用 const 的地方尽量使用 const ，限制变量的赋值。二、集合初始化List<Permission> permissions; permissions.push_back(Permission.BOOK_READ); permissions.push_back(Permission.BOOK_WRITE); check.grantTo(Role.AUTHOR, permissions);这里先给 permission 初始化成了一个 List，这个时候，permissions 虽然存在了，但我们并不会把它传给 grantTo 方法，它还不能直接使用，因为它还缺少必要的信息。然后，我们将 BOOK\_READ 和 BOOK\_WRITE 两个枚举对象添加了进去，这样，这个 permissions 对象才是我们真正需要的那个对象。这种代码是非常常见的，声明一个集合，然后，调用一堆添加的方法，将所需的对象添加进去。解决办法：使用构造函数，初始化的时候就将形参传给它。// c++11 initializer list syntax: List<Permission> permissions = {Permission.BOOK_READ, Permission.BOOK_WRITE}; check.grantTo(Role.AUTHOR, permissions); 这个 List 一旦创建好了，就是不能修改了，对应的实现就是各种添加、删除之类的方法全部都禁用了。Note:变量的声明和赋值是分离的。二者分离带来的问题就是，把赋值的过程与业务处理混杂在一起。发现变量声明与赋值分离一个做法就是在声明前面加上 const，用“不变性”约束代码。

9、 可变的数据：不要让你的代码“失控”
type: item
date: 2024-11-03T15:35:00+08:00
description: 这一类的坏味道：可变的数据。一、满天飞的 Settersetter 几乎是排名第一的坏味道。 相比于读数据，修改是一个更危险的操作 比可变的数据更可怕的是，不可控的变化，而暴露 setter 就是这种不可控的变化。1、常见错误一：使用set来改变成员变量public: void approve(const long bookId) { ... book.setReviewStatus(ReviewStatus.APPROVED); ... }就是因为这里用了 setter。setter 往往是缺乏封装的一种做法。对于缺乏封装的坏味道。实际情况往往是，生成 getter 的同时，setter 也生成了出来。setter 同 getter 一样，反映的都是对细节的暴露。解决办法：移除设值函数（Remove Setting Method），将变化限制在一定的范围之内。用一个函数替代了 setter，也就是把它用行为封装了起来：public： void approve(const long bookId) { ... book.approve(); ... }通过在 Book 类里引入了一个 approve 函数，我们将审核状态封装了起来。class Book { public： void approve() { reviewStatus_ = ReviewStatus.APPROVED; } }虽然审核状态这个字段还是会修改，但你所有的修改都要通过几个函数作为入口。有任何业务上的调整，都会发生在类的内部，只要保证接口行为不变，就不会影响到其它的代码。2、常见错误2：使用连续的set来初始化对象Book book = new Book(); book.setBookId(bookId); book.setTitle(title); book.setIntroduction(introduction);我这个 setter 只是用在初始化过程中，而并不需要在使用的过程去调用，就像这样，实际上，对于这种只在初始化中使用的代码，压根没有必要以 setter 的形式存在，真正需要的是一个有参数的构造函数。解决办法：消除 setter ，有一种专门的重构手法，叫做移除设值函数（Remove Setting Method）。@Getter @Setter class Book { public: Book(BookId bookId, String title, String introduction) : bookId_(bookId), title_(title), introduction_(introduction) { } private: BookId bookId_; String title_; String introduction_; } BookId getBookId(){return bookId_;} BookId setBookId(BookId bookId){bookId_ = bookId;} string getTitle(){return title_;} 不写 setter 的代码并不代表没有 setter。为了清晰的表达这个设计意图，其实就没必要再加上setTitle。可以直接看出来，初始化之后就不会再有set的操作。二、可变的数据使用 setter，一个重要的原因就是它暴露了数据。 把 setter 封装成一个个的函数，实际上是把不可控的修改限制在一个有限的范围内。在这种思路下，可变数据（Mutable Data）就成了一种坏味道。解决可变数据，还有一个解决方案是编写不变类我们怎么设计不变类呢？要做到以下三点：所有的字段只在构造函数中初始化；所有的方法都是纯函数；如果需要有改变，返回一个新的对象，而不是修改已有字段。解决办法：class Book { public: Book approve() { return new Book(..., ReviewStatus.APPROVED, ...); } }回过头来看我们之前改动的“用构造函数消除 setter”的代码，其实就是朝着这个方向在迈进。如果按照这个思路改造我们前面提到的 approve 函数。我们创建出了一个“其它参数和原有 book 对象一模一样，只是审核状态变成了 APPROVED ”的对象。比如，用来表示时间的类。原来的 Date 类里面还有各种 setter，而新增的 LocalDateTime 则一旦初始化就不会再修改了。如果要操作这个对象，则会产生一个新的对象：LocalDateTime twoDaysLater = now.plusDays(2);想要完全消除可变数据是很难做到的，但我们可以尽可能地编写一些不变类。区分类的性质:…

8、 缺乏封装：如何应对火车代码和基本类型偏执问题？
type: item
date: 2024-11-02T15:34:00+08:00
description: 一个重要的观念就是封装，将零散的代码封装成一个又一个可复用的模块。 造成的结果就是：写代码的人认为自己提供了封装，但实际上，我们还是看到许多的代码散落在那里。 这种坏味道违反了迪米特法则一、火车残骸火车残骸的代码就是连续的函数调用，它反映的问题就是把实现细节暴露了出去，缺乏应有的封装。重构的手法是隐藏委托关系，实际就是做封装。 软件行业有一个编程指导原则，叫迪米特法则，可以作为日常工作的指导，规避这种坏味道的出现。String name = book.getAuthor().getName();这段代码表达的是“获得一部作品作者的名字”。作品里有作者信息，想要获得作者的名字，通过“作者”找到“作者姓名”，这就是很多人凭借直觉写出的代码，不过它是有问题的。是不是必须得先了解 Book 和 Author 这两个类的实现细节？也就是说，我们必须得知道，作者的姓名是存储在作品的作者字段里的。这段代码只是用来说明这种类型坏味道是什么样的，在实际工作中，这种在一行代码中有连续多个方法调用的情况屡见不鲜，数量上总会不断突破你的认知。这种坏味道起的名字叫过长的消息链（Message Chains），而有人则给它起了一个更为夸张的名字：火车残骸（Train Wreck），形容这样的代码像火车残骸一般，断得一节一节的。解决办法：解决这种代码的重构手法叫隐藏委托关系（Hide Delegate），说得更直白一些就是，把这种调用封装起来：class Book { ... public： String getAuthorName() { return author_.getName(); } ... } String name = book.getAuthorName();火车残骸这种坏味道的产生是缺乏对于封装的理解，因为封装这件事并不是很多程序员编码习惯的一部分。现在写出一个 getter 往往是 IDE 中一个快捷键的操作，甚至不需要自己手工敲代码。**要想摆脱初级程序员的水平，就要先从少暴露细节开始。**声明完一个类的字段之后，请停下生成 getter 的手，转而让大脑开始工作，思考这个类应该提供的行为。Note:链式调用不一定都是火车残骸。比如builder模式，每次调用返回的都是自身，不牵涉到其他对象，不违反迪米特法则。在软件行业中，有一个编程的指导原则几乎就是针对这个坏味道的，叫做迪米特法则（Law of Demeter），这个原则是这样说的：每个单元对其它单元只拥有有限的知识，而且这些单元是与当前单元有紧密联系的；每个单元只能与其朋友交谈，不与陌生人交谈；只与自己最直接的朋友交谈。二、基本类型偏执基本类型偏执就是用各种基本类型作为模型到处传递，这种情况下通常是缺少了一个模型。解决它，常用的重构手法是以对象取代基本类型，也就是提供一个模型代替原来的基本类型。基本类型偏执不局限于程序设计语言提供的基本类型。 应该构建模型，封装散落的代码。public: double getEpubPrice(const boolean highQuality, const int chapterSequence) { ... }问题就出在返回值的类型上，也就是价格的类型上。虽然价格本身是用浮点数在存储，但价格和浮点数本身并不是同一个概念，有着不同的行为需求。比如，一般情况下，我们要求商品价格是大于 0 的，但 double 类型本身是没有这种限制的。解决办法：这就是很多人使用基本类型（Primitive）作为变量类型思考的角度。但实际上，这种采用基本类型的设计缺少了一个模型。如果补齐这里缺失的模型，我们可以引入一个 Price 类型，这样的校验就可以放在初始化时进行：class Price { public: Price(const double price) { if (price <= 0) { throw new IllegalArgumentException("Price should be positive"); } price_ = price; } private: long price_; }这种引入一个模型封装基本类型的重构手法，叫做以对象取代基本类型（Replace Primitive with Object）。一旦有了这个模型，我们还可以再进一步，比如，如果我们想要让价格在对外呈现时只有两位，在没有 Price 类的时候，这样的逻辑就会散落代码的各处，事实上，代码里很多重复的逻辑就是这样产生的。public: double getDisplayPrice() { BigDecimal decimal = new BigDecimal(price_)； return decimal.setScale(2, BigDecimal.ROUN…

7、 滥用控制语句：出现控制结构，多半是错误的提示
type: item
date: 2024-10-17T15:22:00+08:00
description: 循环和选择语句，可能都是坏味道。 这个坏味道就是滥用控制语句，也就是你熟悉的 if、for 等等一、嵌套的代码public: void distributeEpubs(const int bookId) { List<Epub> epubs = this->getEpubsByBookId(bookId); for (Epub epub : epubs) { if (epub.isValid()) { boolean registered = this->registerIsbn(epub); if (registered) { this->sendEpub(epub); } } } }我们看到了多层的缩进，for 循环一层，里面有两个 if ，又多加了两层。即便不是特别复杂的代码，也有这么多的缩进。解决办法：具体到这段代码，一个着手点是 for 循环，因为通常来说，for 循环处理的是一个集合，而循环里面处理的是这个集合中的一个元素。所以，我们可以把循环中的内容提取成一个函数，让这个函数只处理一个元素。情况1:好的写法 public: void distributeEpubs(const int bookId) { List<Epub> epubs = this->getEpubsByBookId(bookId); for (Epub epub : epubs) { this->distributeEpub(epub)； } } private: void distributeEpub(const Epub epub) { if (epub.isValid()) { boolean registered = this->registerIsbn(epub); if (registered) { this->sendEpub(epub); } } }Note:分解出来 distributeEpub 函数每次只处理一个元素。拆分出来的两个函数在缩进的问题上，就改善了一点。不要写成下面这种形式。情况2: 不好的写法 public: void distributeEpubs(const int bookId) { List<Epub> epubs = this->getEpubsByBookId(bookId); this.distributeEpub(epubs)； } private: void distributeEpub(List<Epub> epubs) { for (Epub epub : epubs) { if (epub.isValid()) { boolean registered = this->registerIsbn(epub); if (registered) { this->sendEpub(epub); } } } }条件表达式和循环常常也是提炼的信号。你可以使用分解表达式来处理条件表达式。如果发现提炼出的循环很难命名，可能是因为其中做了几件事情了。1、拆分循环常常能见到一些身兼数职的循环，它们一次做了两三件事情，不为别的，就因为这样可以只循环一次。一次循环中做了两件不同的事情，那么每次修改循环都要理解这两件事情。情况1: 如果一个循环只计算一个值，那么它直接返回该值即可；情况2: 但如果循环做了太多件事情，那就只得返回结构型数据或者通过局部变量传值了；Tips:如果将情况2重构为情况1这种情况时，需要考虑是否会成为性能瓶颈。举一个粒子，比如一个循环里面有两件事，一个是计算收入总值（数据累加），一个是找出最小收入int total = 0; vector<int> datas = [15, 25, 3, 65, 15]; int min = data[0]; for(auto data : datas) { total += data[i]; if (data[i] < min) { min = data[i]; } }解决办法：使用情况2这种方式进行拆分，但是需要删除涉及到重复计算的逻辑，否则会累加出错。第一步：拆分int total = 0; vector<int> datas = [15, 25, 3, 65, 15]; for(auto data : datas) { total += data[i]; } vector<int> datas = [15, 25, 3, 65, 15]; int min = data[0]; for(auto data : datas) { if (data[i] < min) { min = data[i]; } }第二步：封装int calTotalData(vector<int> datas) { int total = 0; for(auto data …

6、长参数列表：如何处理不同类型的长参数？
type: item
date: 2024-10-01T15:06:00+08:00
description: 函数之间还是要传递信息的，既然不能用全局变量，参数就成了最好的选择，于是乎，只要你想到有什么信息要传给一个函数，就自然而然地把它加到参数列表中，参数列表也就越来越长了。 形参中有bool类型的标志位违背了单一职责原则和接口隔离原则一、聚沙成塔public: void createBook(const String title, const String introduction, const URL coverUrl, const BookType type, const BookChannel channel, const String protagonists, const String tags, const boolean completed) { ... Book book = Book.builder .title(title) .introduction(introduction) .coverUrl(coverUrl) .type(type) .channel(channel) .protagonists(protagonists) .tags(tags) .completed(completed) .build(); repository_.save(book); }这是一个创建作品的函数，我们可以看到，这个函数的参数列表里，包含了一部作品所要拥有的各种信息，比如：作品标题、作品简介、封面 URL、作品类型、作品归属的频道、主角姓名、作品标签、作品是否已经完结等等。但是如果继续拓展呢？我们很自然地就会想到给这个函数增加一个参数。但正如我在讲“长函数”那节课里说到的，很多问题都是这样，每次只增加一点点，累积起来，便不忍直视了。怎么解决这个问题呢？这里所有的参数其实都是和作品相关的，也就是说，所有的参数都是创建作品所必需的。所以，我们可以做的就是将这些参数封装成一个类，一个创建作品的参数类：class NewBookParamters { private: String title; String introduction; URL coverUrl; BookType type; BookChannel channel; String protagonists; String tags; boolean completed; ... }这里你看到了一个典型的消除长参数列表的重构手法：将参数列表封装成对象。这个函数参数列表就只剩下一个参数了，一个长参数列表就消除了。public: void createBook(const NewBookParamters parameters) { ... }还有个疑问，只是把一个参数列表封装成一个类，然后，用到这些参数的时候，还需要把它们一个个取出来，这会不会是多此一举呢？就像这样：public: void createBook(constNewBookParamters parameters) { ... Book book = Book.builder .title(parameters.getTitle()) .introduction(parameters.getIntroduction()) .coverUrl(parameters.getCoverUrl()) .type(parameters.getType()) .channel(parameters.getChannel()) .protagonists(parameters.getProtagonists()) .tags(parameters.getTags()) .completed(parameters.isCompleted()) .build(); repository_.save(book); }解决办法：一个模型的封装应该是以行为为基础的。之前没有这个模型，所以，我们想不到它应该有什么行为，现在模型产生了，它就应该有自己配套的行为，那这个模型的行为是什么呢？从上面的代码我们不难看出，它的行为应该是构建一个作品对象出来class NewBookParamters { private: String title; String introduction; URL coverUrl; BookType type; BookChannel channel; String protagonists; String tags; boolean completed; public: Book newBook() { return Book.builder .title(title) .introduction(introduction) .coverUrl(coverUrl) .type(type) .ch…

5.大类：如何避免写出难以理解的大类？
type: item
date: 2024-10-01T14:52:00+08:00
description: 大类一种表现形式就是我们上节课讲到的长函数，一个类只要有几个长函数，那它就肯定是一眼望不到边了; 还有一种表现形式，类里面有特别多的字段和函数。 类之所以巨大，大部分原因都是违反了单一职责原则。 记住：把类写小，越小越好。一、分模块的程序人类面对复杂事物给出的解决方案是分而治之。所以，我们看到几乎各种程序设计语言都有自己的模块划分方案，从最初的按照文件划分，到后来，使用面向对象方案按照类进行划分，本质上，它们都是一种模块划分的方式。这样，人们面对的就不再是细节，而是模块，模块的数量显然会比细节数量少，人们的理解成本就降低了。实现某个功能时不知道该用哪个函数了，想用哪个函数翻半天都找不到了，只用到一个小功能要引入整个类（类中包含很多无关此功能实现的函数）的时候，这就说明类的行数过多了。如果一个类里面的内容太多，它就会超过一个人的理解范畴，顾此失彼就在所难免了。二、大类的产生想要理解怎么拆分一个大类，我们需要知道，这些类是怎么变成这么大的。1、职责不单一class User { private: long userId; String name; String nickname; String email; String phoneNumber; AuthorType authorType; ReviewStatus authorReviewStatus; EditorType editorType; ... }面对这样一个类时，我们要问的第一个问题就是，这个类里的字段都是必需的吗？问题所在：首先，普通的用户既不是作者，也不是编辑。作者和编辑这些相关的字段，对普通用户来说，都是没有意义的。其次，对于那些成为了作者的用户，编辑的信息意义也不大，因为作者是不能成为编辑的，反之亦然，编辑也不会成为作者，作者信息对成为编辑的用户也是没有意义的。在这个类的设计里面，总有一些信息对一部分人是没有意义，但这些信息对于另一部分人来说又是必需的。之所以会出现这样的状况，关键点就在于，这里只有“一个”用户类。普通用户、作者、编辑，这是三种不同角色，来自不同诉求的业务方关心的是不同的内容。只是因为它们都是这个系统的用户，就把它们都放到用户类里，造成的结果就是，任何业务方的需求变动，都会让这个类反复修改。这种做法实际上是违反了单一职责原则。单一职责原则，它让我们把模块的变化纳入考量。单一职责原则是衡量软件设计好坏的一把简单而有效的尺子，通常来说，很多类之所以巨大，大部分原因都是违反了单一职责原则。而想要破解“大类”的谜题，关键就是能够把不同的职责拆分开来。解决办法：虽然这是一个类，但其实，它把不同角色关心的东西都放在了一起，所以，它变得如此庞大。我们只要把不同的信息拆分开来，问题也就迎刃而解了。下面就是把不同角色拆分出来的结果：class User { private: long userId; String name; String nickname; String email; String phoneNumber; ... }class Author { private: long userId; AuthorType authorType; ReviewStatus authorReviewStatus; ... }class Editor { private: long userId; EditorType editorType; ... }我们拆分出了 Author 和 Editor 两个类，把与作者和编辑相关的字段分别移到了这两个类里面。在这两个类里面分别有一个 userId 字段，用以识别这个角色是和哪个用户相关。这个大 User 类就这样被分解了。Note:评价一个类的职责是否足够单一，我们并没有一个非常明确的、可以量化的标准，因为随着需求的改变，就有可能发生变化。我们可以先写一个粗粒度的类，满足业务需求。随着业务的发展，如果粗粒度的类越来越庞大，代码越来越多，这个时候，我们就可以将这个粗粒度的类，拆分成几个更细粒度的类。这就是所谓的持续重构2、字段未分组大类的产生往往还有一个常见的原因，就是字段未分组。比如，我们看一下上面拆分的结果，那个新的 User 类：class User { private: long userId; String name; String nickname; String email; String phoneNumber; ... }即便相比于原来的 User 类小了许多，这个类依然也不算是一个小类，原因就是，这个类里面的字段并不属于同一种类型的信息。比如，userId、name、nickname 几项，算是用户的基本信息，而 email、phoneNumber 这些则属于用户的联系方式。从需求上看，基本信…

3.重复代码：简单需求到处修改，怎么办？
type: item
date: 2024-09-02T14:24:00+08:00
description: 复制粘贴是最容易产生重复代码的地方，所以，一个最直白的建议就是，不要使用复制粘贴。真正应该做的是，先提取出函数，然后，在需要的地方调用这个函数，把公共的部分先统一掉。 短函数常常能让编译器的优化功能运转更良好，因为短函数可以更容易地被缓存。一、重复的结构 public: void sendBook() { try { service_.sendBook(); } catch (Throwable t) { this.notification_.send(new SendFailure(t))); throw t; } } public: void sendChapter() { try { service_.sendChapter(); } catch (Throwable t) { notification_.send(new SendFailure(t))); throw t; } } public: void startTranslation() { try { service_.startTranslation(); } catch (Throwable t) { notification_.send(new SendFailure(t))); throw t; } }这三段函数业务的背景是：一个系统要把作品的相关信息发送给翻译引擎。所以，结合着代码，我们就不难理解它们的含义，sendBook 是把作品信息发出去，sendChapter 就是把章节发送出去，而 startTranslation 则是启动翻译。我们可以看到，虽然这三个函数调用的业务代码不同，但它们的结构是一致的，其基本流程可以理解为：调用业务函数；如果出错，发通知。上面三段代码如何进行重构，将业务抽象出来，封装一个函数将公共的内容放到里面private: void executeTask(const Runnable& runnable) { try { runnable.run(); } catch (Throwable t) { notification_.send(new SendFailure(t))); throw t; } } public: void sendBook() { executeTask(service_::sendBook); } public: void sendChapter() { executeTask(service_::sendChapter); } public: void startTranslation() { executeTask(service_::startTranslation); }理解到这一点，我们就容易发现结构上的相似之处。比如在上面的例子中，发送作品信息、发送章节、启动翻译之所以看上去是三件不同的事，只是因为它们的动词不同，但是除了这几个动词之外的其它部分是相同的，所以，它们在结构上是重复的。Note:当有catch这种异常场景时应该如何重构的技巧二、if else表达式造成的代码重复只要你看到 if 语句出现，而且 if 和 else 的代码块长得又比较像，多半就是出现了这个坏味道。如果你不想所有人都来玩“找茬”游戏，赶紧消灭它。if (user.isEditor()) { service.editChapter(chapterId, title, content, true); } else { service.editChapter(chapterId, title, content, false); }if 选择的一定是两段不同的业务处理。但只要你稍微看一下，就会发现，if 和 else 两段代码几乎是一模一样的。在经过仔细地“找茬”之后，才能发现，原来是最后一个参数不一样。只有参数不同，是不是和前面说的重复代码是如出一辙的？没错，这其实也是一种重复代码。解决办法：写代码要有表达性。把意图准确地表达出来，是写代码过程中非常重要的一环。显然，这里的 if 判断区分的是参数，而非动作。所以，我们可以把这段代码稍微调整一下，会让代码看上去更容易理解：boolean approved = user.isEditor(); service.editChapter(chapterId, title, content, approved);Note:改进1：这里我把 user.isEditor() 判断的结果赋值给了一个 approved 的变量，而不是直接作为一个参数传给 editChapter，这么做也是为了提高这段代码的可读性。因为 editChapter 最后一个参数表示的是这个章节是否审核通过。通过引入 approved 变量，我们可以清楚地看到，一个章节审核是否通过的判断条件是“用户是…

2.如何正确的使用英文命名
type: item
date: 2024-09-02T14:08:00+08:00
description: 常见的命名规则是：类名是一个名词，表示一个对象，而方法名则是一个动词，或者是动宾短语，表示一个动作。eg. 动词+名词，不要是名词+动词一、命名多长最合适在足够表达其含义的情况下，命名当然是越短越好。这样一方面能让命名短一些，另一方面又不影响阅读理解，比如，sec 表示 second、str 表示 string、num 表示 number、doc 表示 document。对于作用域比较小的变量，我们可以使用相对短的命名，比如一些函数内的临时变量。对于类名这种作用域比较大的，我更推荐用长的命名方式。二、违反语法规则的命名public: void completedTranslate(const List<ChapterId> chapterIds) { List<Chapter> chapters = repository.findByChapterIdIn(chapterIds); chapters.forEach(Chapter::completedTranslate); repository.saveAll(chapters); }初看之下，这段代码写得还不错，它要做的是将一些章节的信息标记为翻译完成。似乎函数名也能反映这个意思，但仔细一看你就会发现问题。作者想表达的是“完成翻译”，因为是已经翻译完了，所以，他用了完成时的 completed，而翻译是 translate。这个函数名就成了 completedTranslate。但是上面这个并不是动宾短语：completedTranslate 并不是一个有效的动宾结构。如果把这个名字改成动宾结构，只要把“完成”译为 complete，“翻译”用成它的名词形式 translation 就可以了。所以，这个函数名可以改成 completeTranslation：public: void completeTranslation(const List<ChapterId> chapterIds) { List<Chapter> chapters = repository.findByChapterIdIn(chapterIds); chapters.forEach(Chapter::completeTranslation); repository.saveAll(chapters); }再比如：一个函数名是 retranslation，其表达的意图是重新翻译，但作为函数名，它应该是一个动词，所以，正确的命名应该是 retranslate。三、不准确的英语词汇public: enum ChapterAuditStatus { PENDING, APPROVED, REJECTED; }如果你用审核作为关键字去字典网站上搜索，确实会得到 audit 这个词。public: enum ChapterReviewStatus { PENDING, APPROVED, REJECTED; }我把 audit 和 review 同时放到了搜索引擎里查了一下。原来，audit 会有更官方的味道，更合适的翻译应该是审计，而 review 则有更多核查的意思，二者相比，review 更适合这里的场景。于是，章节的审核状态也统一使用了 review：这种情况的解决办法：；最好的解决方案还是建立起一个业务词汇表，千万不要臆想。一般情况下，我们都可以去和业务方谈，共同确定一个词汇表，包含业务术语的中英文表达。这样在写代码的时候，你就可以参考这个词汇表给变量和函数命名。这个词汇表: 一方面，词汇表给出的都是业务术语，同时也给出了在特定业务场景下的含义；另一方面，它也给出了相应的英文，省得你费劲心思去思考。当你遇到了一个词汇表中没有的术语怎么办呢？那就需要找出这个术语相应的解释，然后，补充到术语表里。四、英语单词拼写错误对今天的程序员来说，工具已经很进步了，像 IntelliJ IDEA 这样的 IDE 甚至可以给你提示代码里有拼写错误（typo），不少支持插件的工具也都有自己的拼写检查插件，比如Visual Studio Code的**Code Spell Checker插件**五、其他简单的坏味道使用拼音进行命名；使用不恰当的单词简写（比如，多个单词的首字母，或者写单词其中的一部分）。六、命名要可读、可搜索我这里所说的“可读”，指的是不要用一些特别生僻、难发音的英文单词来命名。在命名的时候，最好能符合整个项目的命名习惯。大家都用“selectXXX”表示查询，你就不要用“queryXXX”；大家都用“insertXXX”表示插入一条数据，你就要不用“addXXX”，统一规约是很重要的，能减少很多不必要的麻烦。

4 长函数：为什么你总是不可避免地写出长函数？
type: item
date: 2024-09-01T14:38:00+08:00
description: 从不同的抽象层次理解单一职责一、长函数的产生不过，限制函数长度，是一种简单粗暴的解决方案。最重要的是你要知道，长函数本身是一个结果，如果不理解长函数产生的原因，还是很难写出整洁的代码。接下来，我们就来看看长函数是怎么产生的。 如何确定该提炼哪一段代码：寻找注释1、多长的函数才算“长”？那就是不要超过一个显示屏的垂直高度。比如，在我的电脑上，如果要让一个函数的代码完整地显示在 IDE 中，那最大代码行数不能超过 50。这个说法我觉得挺有道理的。因为超过一屏之后，在阅读代码的时候，为了串联前后的代码逻辑，就可能需要频繁地上下滚动屏幕，阅读体验不好不说，还容易出错2、以性能为由不过，这个观点在今天是站不住的。性能优化不应该是写代码的第一考量。一方面，一门有活力的程序设计语言本身是不断优化的，无论是编译器，还是运行时，性能都会越来越好；另一方面，可维护性比性能优化要优先考虑，当性能不足以满足需要时，我们再来做相应的测量，找到焦点，进行特定的优化。这比在写代码时就考虑所谓性能要更能锁定焦点，优化才是有意义的。现代编程语言几乎已经完全免除了进程内的函数调用开销。3、平铺直叙一种最常见的原因也会把代码写长，那就是写代码平铺直叙，把自己想到的一点点罗列出来。//这段代码来自java public void executeTask() { ObjectMapper mapper = new ObjectMapper(); CloseableHttpClient client = HttpClients.createDefault(); List<Chapter> chapters = this.chapterService.getUntranslatedChapters(); for (Chapter chapter : chapters) { // Send Chapter SendChapterRequest sendChapterRequest = new SendChapterRequest(); sendChapterRequest.setTitle(chapter.getTitle()); sendChapterRequest.setContent(chapter.getContent()); HttpPost sendChapterPost = new HttpPost(sendChapterUrl); CloseableHttpResponse sendChapterHttpResponse = null; String chapterId = null; try { String sendChapterRequestText = mapper.writeValueAsString(sendChapterRequest); sendChapterPost.setEntity(new StringEntity(sendChapterRequestText)); sendChapterHttpResponse = client.execute(sendChapterPost); HttpEntity sendChapterEntity = sendChapterPost.getEntity(); SendChapterResponse sendChapterResponse = mapper.readValue(sendChapterEntity.getContent(), SendChapterResponse.class); chapterId = sendChapterResponse.getChapterId(); } catch (IOException e) { throw new RuntimeException(e); } finally { try { if (sendChapterHttpResponse != null) { sendChapterHttpResponse.close(); } } catch (IOException e) { // ignore } } // Translate Chapter HttpPost translateChapterPost = new HttpPost(translateChapterUrl); CloseableHttpResponse translateChapterHttpResponse = null; try { TranslateChapterRequest translateChapterRequest = new TranslateChapterRequest(); translateChapterRequest.setChapterId(cha…

1. 缺乏业务含义的命名：如何精准命名
type: item
date: 2024-08-09T14:03:00+08:00
description: 大到项目名、模块名、包名、对外暴露的接口，小到类名、函数名、变量名、参数名，只要是做开发，我们就逃不过“起名字”这一关一、坏味道一：不精准的命名命名过于宽泛，不能精准描述，这是很多代码在命名上存在的严重问题，也是代码难以理解的根源所在。class Demo { public: void processChapter(long chapterId) { Chapter chapter = repository_.findByChapterId(chapterId); if (chapter == nullptr) { throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]"); } chapter.setTranslationState(TranslationState.TRANSLATING); repository_.save(chapter); } }问题就出在函数名上。这个函数的名字叫 processChapter（处理章节），这个函数确实是在处理章节，但是，这个名字太过宽泛。将章节的翻译状态改成翻译中或者将章节的翻译状态改成翻译完都可以称为处理章节，那么处理章节就是一个过于宽泛的名字，没有错，但不精准。1、常见的错误我们看看下面这些词是不是经常出现在你的代码里：data、info、flag、process、handle、build、maintain、manage、modify 等等。这些名字都属于典型的过于宽泛的名字，只要稍微仔细想想，类似的名字你一定还能想出不少来。回到前面那段代码上，首先，命名要能够描述出这段代码在做的事情。这段代码在做的事情就是“将章节修改为翻译中”。那是不是它就应该叫 changeChapterToTranslating 呢？我们为什么要把翻译状态修改成翻译中，这一定是有原因的，也就是意图。函数封装的意义在于我们不想知道那么多的细节。一个好的名字应该描述意图，而非细节。我们把翻译状态修改成翻译中，是因为我们在这里开启了一个翻译的过程。所以，这段函数应该命名 startTranslation。二、坏味道二：用技术术语命名这是一种不费脑子的命名方式，但是，这种命名却会带来很多问题，因为它是一种基于实现细节的命名方式。List<Book> bookList = service.getBooks();这个 bookList 变量之所以叫 bookList，原因就是它声明的类型是 List。这种命名在代码中几乎是随处可见的，比如 xxxMap、xxxSet。Bad: 我需要把这个变量的类型从 List 改成 Set。变量类型你一定会改，但变量名你会改吗？这还真不一定，一旦出现遗忘，就会出现一个奇特的现象，一个叫 bookList 的变量，它的类型是一个 Set。这样，一个新的混淆就此产生了。List<Book> books = service.getBooks();也许你发现了，这个名字其实更简单，但从表意的程度上来说，它却是一个更有效的名字。事实上，在实际的代码中，技术名词的出现，往往就代表着它缺少了一个应有的模型。1、改进点：应该用业务语言写代码无论是不精准的命名也好，技术名词也罢，归根结底，体现的是同一个问题：对业务理解不到位。 编写可维护的代码要使用业务语言。怎么才知道自己的命名是否用的是业务语言呢？一种简单的做法就是，把这个词讲给产品经理，看他知不知道是怎么回事。 改进函数名字的好办法：先写一句注释描述这个函数的用途，再把这句注释变成函数的名字让每个人根据自己的理解来命名，确实就有可能出现千奇百怪的名字，所以，一个良好的团队实践是，建立团队的词汇表，让团队成员有信息可以参考。public: void approveChapter(long chapterId, long **userId**) { ... }这个函数的意图是，确认章节内容审核通过。这里有一个问题，chapterId 是审核章节的 ID，这个没问题，但 userId 是什么呢？了解了一下背景，我们才知道，之所以这里要有一个 userId，是因为这里需要记录一下审核人的信息，这个 userId 就是审核人的 userId。通过业务的分析，我们会发现，这个 userId 并不是一个好的命名，因为它还需要更多的解释，更好的命名是 reviewerUserId，之所以起这个名字，因为这个用户在这个场景下扮演的角色是审核人（Reviewer）。public: void approveChapter(long chapterId, long **reviewerUserId**) { ... }这个坏味道也是一种不精准的命名，但它不是那种一眼可见的坏味道，…