代码之丑-坏味道

> 但能够把代码写得更具可维护性,这是一个程序员从业余迈向职业的第一步。

坏味道之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 时,曾经讲过二者的区别,声明式的代码体现的意图,是更高层面的抽象,把意图和实现分开,从某种意义上来说,也是一种分离关注点。

 

一次性完成变量的初始化。

posted @ 2022-09-19 20:29  Other+  阅读(142)  评论(0编辑  收藏  举报