命名

命名不具有业务含义

命名不清晰

// 函数命名不清晰
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);
}

用技术术语命名

技术名词的出现,往往就代表着它缺少了一个应有的模型。
这里就只是获取一些书,从业务逻辑上,不需要关注是 List 还是 Set 形式。

// 下面的更好
List<Book> bookList = service.getBooks();
List<Book> books = service.getBooks();

这里就是需要一个缓存,不需要知道是 Redis 缓存还是本地缓存。

Book cachedBook = redisBookStore.get(isbn);
Book cachedBook = cache.get(isbn);

命名不具有英语语法

  • 违法语法规则
  • 不准确的英语词汇 —— 建立团队词汇表(名称/英文/说明)

函数

函数行数过长

Python、Ruby 等表达能力强的动态语言,函数代码尽量 5 行内。
Java 等表达能力弱等动态语言,函数代码尽量 10 行内。
若因为 bool 型的标记参数使得函数过长,应该拆分出来。
在 Java 中,我们就可以把代码行的约束加到 CheckStyle 的配置文件中,就像下面这样:

<module name="MethodLength">
	<property name="tokens" value="METHOD_DEF"/>
	<property name="max" value="20"/>
	<property name="countEmpty" value="false"/>
</module>

函数的参数过多

函数参数过多 & 变化频率相同时,可以考虑将其封装为一个类。
函数参数过多 & 变化频率不同时,静态不变的参数作为这个函数所在类的一个字段。
优化前

public void createBook(final String title,
					 final String introduction,
					 final URL coverUrl,
					 final BookType type,
					 final BookChannel channel,
					 final String protagonists,
					 final String tags,
					 final boolean completed) {
	...
	Book book = Book.builder
	  .title(title)
	  .introduction(introduction)
	  .coverUrl(coverUrl)
	  .type(type)
	  .channel(channel)
	  .protagonists(protagonists)
	  .tags(tags)
	  .completed(completed)
	  .build();
	this.repository.save(book);
}

优化后

public void createBook(final NewBookParamters parameters) {
	...
	Book book = parameters.newBook();
	this.repository.save(book);
}

类的字段过多

最容易产生大类的原因在于职责的不单一
比如,将 User 变成 User、Author、Editor 三个类。
大类的产生往往还有一个常见的原因,就是字段未分组
比如,将 User 中的 email、phone 属性拿出来作为 Contact 类。
后者有点类似于 DDD 中实体和值对象之间的关系。

对象的封装不够完善

对象的过长消息链

若存在对于对象的过长消息链,考虑是否封装的不够完善。

// 链式获取书籍的作者名字
String name = book.getAuthor().getName();
// 书籍的名字直接作为 Book 的某个方法,使用者无需关心 Book 内如何存储作者名字
String name = book.getAuthorName();

用基本类型表示对象

将对象直接用基本数据类型/字符串来替代。
对于下面的例子来说,如果使用价格的地方比较多,应该将其封装为一个对象,减少校验次数。

public double getEpubPrice(final boolean highQuality, final int chapterSequence) {
	...
	if (price <= 0) {
		throw new IllegalArgumentException("Price should be positive");
	}
}

这样封装为对象后,代码更简洁,后续基于价格的改动也更容易,如让价格在对外呈现时只有两位。

class Price {
	private long price;
	public Price(final double price) {
		if (price <= 0) {
			throw new IllegalArgumentException("Price should be positive");
		}
		this.price = price;
	}
}

注意对象的可变性

无业务含义的 setter 方法

如果可以不创建任何一个 setter 方法,而是创建有业务含义的方法。
这和“函数命名方式”类似,不同点在于,类的 setter 方法可能会导致不可控的变化。
例如下面的例子,有可能外部调用的时候没有按照规定传入参数,setter 中也没有进行输入检查。

public void approve(final long bookId) {
	...
	book.setReviewStatus(ReviewStatus.APPROVED);
	...
}

更合理的方法是下面这种,避免将不必要的内容暴露出来。

public void approve(final long bookId) {
...
book.approve();
...
}
 
class Book {
	public void approve() {
		this.reviewStatus = ReviewStatus.APPROVED;
	}
}

另一种消除可变性的方法就是,设置成不变类。
值对象就要设计成不变类,实体类则要限制数据变化。

class Book {
	public Book approve() {
		return new Book(..., ReviewStatus.APPROVED, ...);
	}
}

依赖继承

多使用组合/聚合的形式,而不是盲目使用继承来实现。

语句

声明和赋值相隔较远

变量声明和赋值相隔较远

在声明前面增加 final,用不变性的限制约束代码。此时,赋值时就会选择通过函数来进行。
优化前

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;
}

集合声明后未立刻添加元素

除此之外,对于集合来说,Java 可以 9 还新增了专门的工具类来进行初始化。

List<Permission> permissions = List.of(
	Permission.BOOK_READ,
	Permission.BOOK_WRITE
);

重复结构的代码

优化前

@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;
}
}

优化后

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);
}

控制语句嵌套过多

For 嵌套过多,应该将被迭代部分转换为函数。
If 嵌套过多,应该在不满足的情况时返回。
Else 层数过多,也应该在不满足的情况时返回。
Switch 复杂,考虑用多态来代替。
优化前

public void distributeEpubs(final long bookId) {
	List<Epub> epubs = this.getEpubsByBookId(bookId);
	for (Epub epub : epubs) {
		if (epub.isValid()) {
			boolean registered = this.registerIsbn(epub);
			if (registered) {
				this.sendEpub(epub);
			}
		}
	}
}

优化后

public void distributeEpubs(final long bookId) {
	List<Epub> epubs = this.getEpubsByBookId(bookId);
	for (Epub epub : epubs) {
		this.distributeEpub(epub);
	}
}
private void distributeEpub(final Epub epub) {
	if (!epub.isValid()) {
		return;
	}
	boolean registered = this.registerIsbn(epub);
	if (!registered) {
		return;
	}
	this.sendEpub(epub);
}

使用不合理的全局变量

Java 类中 static 对象和语句块、Python 中的全局变量,这些都属于不容易测试的代码,最好不要轻易去写。

业务

依赖混乱

接口和业务之间缺少防腐层

缺少防腐层,会让请求对象传导到业务代码中,造成了业务与外部接口的耦合,也就是业务依赖了一个外部通信协议。一般来说,业务的稳定性要比外部接口高,这种反向的依赖就会让业务一直无法稳定下来,继而在日后带来更多的问题。解决方案自然就是引入一个防腐层,将业务和接口隔离开来。

业务代码中出现具体实现类

业务代码中出现具体的实现类,实际上是违反了依赖倒置原则。因为违反了依赖倒置原则,业务代码也就不可避免地受到具体实现的影响,也就造成了业务代码的不稳定。

参考链接