微信公众号:牛奶 Yoka 的小屋
有任何问题。欢迎来撩~
最近更新:2024/08/03
[大家好,我是牛奶。]
我在上一篇文章打开IDEA,程序员思考的永远只有两件事!中,通过代码命名、重复代码、合格方法三个章节,着重讲解了24种代码坏味道最常见的五种:注释、命名、重复代码、过长函数、过长的参数列表。在这篇文章中,我将通过对象数据、对象关系、代码表达力及重复的坏味道四个章节,再详细讲一讲剩下的19种坏味道。
《重构》的大佬Martin Fowler给出24种代码坏味道,但我看未必。我认为有些坏味道犯了“重复坏味道”的问题,所以我把它们汇总到最后一章重复的坏味道中,大家看完其他坏味道再看这些,便可很快理解;有些坏味道只是代码啰嗦了点,并不会对业务逻辑产生什么实质影响,我将他们放在了代码表达力的章节;还有些坏味道根因类似,比如基本类型偏执和重复的switch,所以我把它们放到一起,方便理解。本篇文章的阅读导图如下:
代码坏味道
总之,当你看完所有的坏味道,会发现大佬翻来覆去,讲的都是那几类问题。愿此篇文章,能使诸君对坏味道代码的敏感度有所提升!
对象数据定义时有哪些坑?
普通数据可变问题
可变数据是Martin Fowler大佬在《重构》第二版新增的坏味道。这个坏味道很是反直觉,我琢磨了半天,才咂巴出点味道。突然想起句名人名言:
我横竖睡不着,仔细看了半夜,才从字缝里看出字来,整段都写着几个字是“别让数据可变”!——坡迅
什么是可变数据?
任何在赋值之后仍可以修改的变量称为可变数据。例如拥有Getter方法和Setter方法的类变量(Getter方法的返参可变导致变量可变);再或者拥有类似Setter设置变更值方法的变量。(咱就说这两类变量常不常用!)
为啥可变数据很危险?
一句话概括,你不知道数据会在哪里被何人以什么方式修改。下面给一个银行存钱的简单案例,上代码:
import java.math.BigDecimal;
public class BankAccount {
private BigDecimal balance;
public BankAccount(BigDecimal initialBalance) {
this.balance = initialBalance;
}
// 存款方法
public void deposit(BigDecimal amount) {
balance = balance.add(amount);
}
// 取款方法
public void withdraw(BigDecimal amount) {
if (amount.compareTo(balance) < 1) {
balance = balance.subtract(amount);
} else {
System.out.println("Insufficient funds");
}
}
// 获取余额方法
public BigDecimal getBalance() {
return balance;
}
}
BankService类:
public class BankService {
private BankAccount account;
public BankService(BankAccount account) {
this.account = account;
}
// 服务方法,意外地修改了账户余额
public void performService() {
// 假设这里执行了一些操作,需要修改账户余额
account.deposit(BigDecimal.valueOf(100)); // 意外地给账户增加了100元
}
}
主函数:
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount(BigDecimal.valueOf(1000)); // 初始余额1000元
BankService service = new BankService(account);
// 显示初始余额
System.out.println("Initial balance: " + account.getBalance());
// 执行服务,意外修改了账户余额
service.performService();
// 显示修改后的余额,意外地增加了100元
System.out.println("Balance after service: " + account.getBalance());
}
}
代码中的银行余额balance为可变变量,该变量会被传递到银行服务类BankService中时,该类可能是另一位同事负责开发,他在执行服务的方法中意外修改了银行余额,最终导致BankAccount类中的可变变量发生了非预期的变化。
数据可变带来的不可控和难定位等风险远比我们想象严重,甚至出现了完全建立在“数据永不改变”概念基础上的软件开发流派——函数式编程。在该编程范式中,如果要更新一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变。
我仔细想了想,发现写过的大部分类中的可变变量,基本都是一次赋值,需要再次赋值都会new出一个新类。所以,针对可变数据,Martin Fowler给出的优化建议是:
-
将可变数据设置为私有且不可变状态(private final)
-
将参数赋值优化为使用有参构造函数
-
必须二次赋值时使用新的数据副本
作者倡导类的创建和类变量的赋值要同时进行,创建类的时候就要初始化变量,自此不可更改,非要更改就重新创建一个新类。
上述银行余额问题,满足优化建议第二点,但不满足第一三点。而银行余额是必须要存取,因此,BankAccount类代码优化后为:
public class BankAccount {
private final BigDecimal balance;
public BankAccount(BigDecimal initialBalance) {
this.balance = initialBalance;
}
public BankAccount deposit(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive.");
}
return new BankAccount(this.balance.add(amount));
}
public BankAccount withdraw(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0 || amount.compareTo(this.balance) > 0) {
throw new IllegalArgumentException("Invalid withdrawal amount.");
}
return new BankAccount(this.balance.subtract(amount));
}
public BigDecimal getBalance() {
return balance;
}
}
优化后,余额balance成为不可变变量,依然作为构造函数参数不变。同时存取方法返回的是新的实例对象,这样即使在BankService类的performService方法中误操作,最后在主函数中打印account.getBalance()银行余额,依然是1000元,不会平白无故新增100元。大家可以在编辑器中进行简单测试。
新的问题随之而来,如果有个类中有1000个变量,难道往构造函数中传1000个参数么?答案是,不用,用构建器替代构造函数(构建器可直接使用Build注解)。
上代码:
@Data
public class BankAccount {
public BigDecimal balance;
public String amount;
}
应优化为
@Builder
public class BankAccount {
private final BigDecimal balance;
private final String amount;
}
构建器使用方式:
BankAccount bankAccount = BankAccount.builder().balance(0.00).amount("1000").build();
使用构建器后,就可以选择性的赋值参数,添加参数也不影响旧代码。提高了代码的扩展性和可维护性。
如果可变数据赋值固定,赋值总是那几个值,比如订单状态等。开源项目Moco作者郑晔大佬还提出一个优化建议:将可变数据的赋值操作封装为一个不带参的类方法,取代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