代码之丑-坏味道
> 但能够把代码写得更具可维护性,这是一个程序员从业余迈向职业的第一步。
坏味道之Setter
public void approve(final long bookId) { book.setReviewStatus(ReviewStatus.APPROVED); }
这个审核的状态是作品的一个内部状态,为什么服务需要知道它呢?也就是说,这里通过 setter,将一个类的内部行为暴露了出来,这是一种破坏封装的做法。
public void approve(final long bookId) { ... book.approve(); ... }
setter 的出现,是对于封装的破坏,它把一个类内部的实现细节暴露了出来。我在《软件设计之美》中讲过,面向对象的封装,关键点是行为,而使用 setter 多半只是做了数据的聚合,缺少了行为的设计,这段代码改写后的 approve 函数,就是这里缺少的行为。
坏味道之不精准的命名
命名过于宽泛,不能精准描述,这是很多代码在命名上存在的严重问题,也是代码难以理解的根源所在。
我们看看下面这些词是不是经常出现在你的代码里:data、info、flag、process、handle、build、maintain、manage、modify 等等。这些
名字都属于典型的过于宽泛的名字,当这些名字出现在你的代码里
所以,一个好的名字应该描述意图,而非细节。
坏味道之用技术术语命名
我们再来看一段代码:
List<Book> bookList = service.getBooks();
可以说这是一段常见得不能再常见的代码了,但这段代码却隐藏另外一个典型得不能再典型的问题:用技术术语命名。
这个 bookList 变量之所以叫 bookList,原因就是它声明的类型是 List。这种命名在代码中几乎是随处可见的,比如 xxxMap、xxxSet。
这是一种不费脑子的命名方式,但是,这种命名却会带来很多问题,因为它是一种基于实
现细节的命名方式。
我们都知道,编程有一个重要的原则是面向接口编程,这个原则从另外一个角度理解,就是不要面向实现编程,因为接口是稳定的,而实现是易变的。
虽然这里我们只是以变量为例说明了以技术术语命名存在的问题,事实上,在实际的代码中,技术名词的出现,往往就代表着它缺少了一个应有的模型。
坏味道之大类
如果一个类里面的内容太多,它就会超过一个人的理解范畴,顾此失彼就在所难免了。
原因:
- 职责不单一
- 字段未分组
那请记住:把类写小,越小越好
坏味道之参数标记
public void editChapter(final long chapterId, final String title, final String content, final boolean apporved) { ... }
使用标记参数,是程序员初学编程时常用的一种手法,不过,正是因为这种手法实在是太好用了,造成的结果就是代码里面彩旗(flag)飘飘,各种标记满天飞
解决标记参数,一种简单的方式就是,将标记参数代表的不同路径拆分出来。
// 普通的编辑,需要审核 public void editChapter(final long chapterId, final String title, final String content) { ... } // 直接审核通过的编辑 public void editChapterWithApproval(final long chapterId, final String title, final String content) { ... }
在重构中,这种手法叫做移除标记参数(Remove Flag Argument)。
坏味道之else
有一个衡量代码复杂度常用的标准,叫做圈复杂度(Cyclomaticcomplexity,简称 CC)。在圈复杂度的判定中,循环和选择语句占有重要的地位。
只要我们能够消除嵌套,消除 else,代码的圈复杂度就不会很高,理解和维护的成本自然也就会随之降低。
public double getEpubPrice(final boolean highQuality, final int chapterSequenc double price = 0; if (highQuality && chapterSequence > START_CHARGING_SEQUENCE) { price = 4.99; } else if (sequenceNumber > START_CHARGING_SEQUENCE && sequenceNumber <= FURTHER_CHARGING_SEQUENCE) { price = 1.99; } else if (sequenceNumber > FURTHER_CHARGING_SEQUENCE) { price = 2.99; } else { price = 0.99; } return price; }
正解
public double getEpubPrice(final boolean highQuality, final int chapterSequenc if (highQuality && chapterSequence > START_CHARGING_SEQUENCE) { return 4.99; } if (sequenceNumber > START_CHARGING_SEQUENCE && sequenceNumber <= FURTHER_CHARGING_SEQUENCE){ return 1.99; } if (sequenceNumber > FURTHER_CHARGING_SEQUENCE) { return 2.99; } return 0.99; }
坏味道之火车残骸
String name = book.getAuthor().getName();
Martin Fowler 在《重构》中给这种坏味道起的名字叫过长的消息链(Message Chains),而有人则给它起了一个更为夸张的名字:火车残骸(Train Wreck),形容这样的代码像火车残骸一般,断得一节一节的。
解决这种代码的重构手法叫隐藏委托关系(Hide Delegate),说得更直白一些就是,把这种调用封装起来:
class Book { ... public String getAuthorName() { return this.author.getName(); } ... } String name = book.getAuthorName();
坏味道之变量声明与赋值分离
EpubStatus status = null; CreateEpubResponse response = createEpub(request); if (response.getCode() == 201) { status = EpubStatus.CREATED; } else { status = EpubStatus.TO_CREATE; }
变量初始化最好一次性完成
这种代码真正的问题就是不清晰,变量初始化与业务处理混在在一起。通常来说,这种代码后面紧接着就是一大堆更复杂的业务处理。当代码混在一起的时候,我们必须小心翼翼地从一堆业务逻辑里抽丝剥茧,才能把逻辑理清,知道变量到底是怎么初始化的。很多代码难读,一个重要的原因就是把不同层面的代码混在了一起。
final CreateEpubResponse response = createEpub(request); final EpubStatus status = toEpubStatus(response); private EpubStatus toEpubStatus(final CreateEpubResponse response) { if (response.getCode() == 201) { return EpubStatus.CREATED; } return EpubStatus.TO_CREATE; }
坏味道之集合初始化
List<Permission> permissions = new ArrayList<>(); permissions.add(Permission.BOOK_READ); permissions.add(Permission.BOOK_WRITE); check.grantTo(Role.AUTHOR, permissions);
理论上也是声明和赋值分离里,可以借助新api(先来看看 Java 9 之后的写法:)
List<Permission> permissions = List.of( Permission.BOOK, Permission.BOOK ); check.grantTo(Role.AUTHOR, permissions);
或者Guava
List<Permission> permissions = ImmutableList.of( Permission.BOOK_READ, Permission.BOOK_WRITE ); check.grantTo(Role.AUTHOR, permissions);
Map
private static Map<Locale, String> CODE_MAPPING = new HashMap<>(); static { CODE_MAPPING.put(LOCALE.ENGLISH, "EN"); CODE_MAPPING.put(LOCALE.CHINESE, "CH"); }
如果我们能够用一次性声明的方式,这个单独的 static 块就是不需要的:
private static Map<Locale, String> CODE_MAPPING = ImmutableMap.of( LOCALE.ENGLISH, "EN", LOCALE.CHINESE, "CH" );
我在《软件设计之美》专栏中讲 DSL 时,曾经讲过二者的区别,声明式的代码体现的意图,是更高层面的抽象,把意图和实现分开,从某种意义上来说,也是一种分离关注点。
一次性完成变量的初始化。