45、代码坏味道:依恋情结 - 实战搬移函数与策略模式
今天我们来聊一个很常见的坏味道——依恋情结。
说白了,就是一个类太“粘”另一个类了。它老是去访问别人的数据,调用别人的方法,搞得自己像个“跟屁虫”。我见过不少项目,代码写着写着,一个类里全是别的类的getter调用,自己的逻辑反而没多少。
为什么会这样?
因为人都有惰性。你想想看,写一个新功能时,发现某个类里已经有现成的数据,顺手就调过来了。一次两次还好,时间长了,这个类就变成了“数据中转站”,而那个被依赖的类则越来越臃肿。
嗯,今天我们就用两个实战案例,把这个问题彻底讲透。
什么是依恋情结?
依恋情结(Feature Envy)的定义很简单:一个方法对另一个类的兴趣,超过了对自身所在类的兴趣。
判断标准也很直观:
- 方法里大量调用其他类的getter
- 方法里频繁访问其他类的属性
- 方法里多次调用其他类的方法,而自己类的成员几乎没用
我个人的习惯是:如果一个方法里,超过一半的代码都在跟别的类“打交道”,那就要警惕了。
核心原则:把方法搬移到它真正“依恋”的那个类里去。
案例一:搬移函数
先看一个简单的例子。假设我们有一个订单系统:
public class Order {
private Customer customer;
private double amount;
public double calculateDiscount() {
// 这个方法大量依赖 Customer 类的数据
if (customer.isVIP()) {
return amount * 0.2;
} else if (customer.getTotalOrders() > 10) {
return amount * 0.1;
} else {
return amount * 0.05;
}
}
}
你看,calculateDiscount 方法里,几乎每一行都在调用 customer 的东西。它依恋的是 Customer 类,而不是 Order 类。
怎么改?搬移函数。
public class Customer {
private boolean vip;
private int totalOrders;
public double calculateDiscount(double amount) {
if (isVIP()) {
return amount * 0.2;
} else if (getTotalOrders() > 10) {
return amount * 0.1;
} else {
return amount * 0.05;
}
}
}
public class Order {
private Customer customer;
private double amount;
public double calculateDiscount() {
return customer.calculateDiscount(amount);
}
}
改完之后,Order 的 calculateDiscount 只剩一行委托调用。逻辑归属清晰了,Customer 自己决定怎么打折,而不是让 Order 去猜。
小技巧:搬移函数时,如果原方法里用到了当前类的数据,就把这些数据作为参数传过去。就像上面把 amount 传给了 Customer。
案例二:策略模式
有时候,依恋情结没那么简单。一个方法可能依恋多个不同的类,或者依恋的逻辑太复杂,单纯搬移函数解决不了。
我曾经在一个支付系统里遇到过这种情况。一个 PaymentService 类里,有个 calculateFee 方法,它要判断支付方式(微信、支付宝、银行卡),然后去不同的类里拿费率数据。代码长得吓人,全是 if-else 和 getter 调用。
这就是典型的依恋情结——PaymentService 太“粘”那些支付方式类了。
这时候,策略模式就派上用场了。
先定义一个策略接口:
public interface PaymentStrategy {
double calculateFee(double amount);
}
然后每个支付方式实现自己的策略:
public class WeChatPayStrategy implements PaymentStrategy {
@Override
public double calculateFee(double amount) {
// 微信的费率逻辑
return amount * 0.006;
}
}
public class AlipayStrategy implements PaymentStrategy {
@Override
public double calculateFee(double amount) {
// 支付宝的费率逻辑
return amount * 0.0055;
}
}
public class BankCardStrategy implements PaymentStrategy {
@Override
public double calculateFee(double amount) {
// 银行卡的费率逻辑
return amount * 0.008;
}
}
最后,PaymentService 只需要持有策略对象:
public class PaymentService {
private PaymentStrategy strategy;
public PaymentService(PaymentStrategy strategy) {
this.strategy = strategy;
}
public double calculateFee(double amount) {
return strategy.calculateFee(amount);
}
}
你看,PaymentService 不再依恋任何具体的支付方式类。它只依赖一个抽象的 PaymentStrategy 接口。每个策略类自己管理自己的费率逻辑,互不干扰。
注意:策略模式不是万能的。如果只有两三种简单的分支判断,用 if-else 反而更清晰。别为了用模式而用模式。
知识体系图
下面这张图帮你理清依恋情结的解决思路:
什么时候用搬移函数,什么时候用策略模式?
我个人的经验是:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 方法只依恋一个类 | 搬移函数 | 简单直接,改动最小 |
| 方法依恋多个类,且逻辑相似 | 策略模式 | 消除重复,便于扩展 |
| 依恋逻辑未来可能频繁变化 | 策略模式 | 开闭原则,新增策略不影响现有代码 |
| 依恋逻辑只有两三种固定分支 | 搬移函数 + 简单判断 | 过度设计反而增加复杂度 |
避坑指南
我曾经在一个项目里,看到同事把整个 Order 类的方法全搬到了 Customer 里。结果 Customer 变得巨大无比,什么订单逻辑、折扣逻辑、物流逻辑全塞进去了。
这就是矫枉过正。
搬移函数不是“把所有东西都搬过去”,而是把真正属于那个类的逻辑搬过去。如果一个方法既用了 Customer 的数据,又用了 Order 的数据,那它可能属于一个独立的第三个类,或者应该用策略模式来拆分。
我的建议:每次搬移前,先问自己一个问题——“这个方法如果放在目标类里,读起来是不是更自然?” 如果答案是肯定的,那就搬。如果犹豫,那就再想想。
总结
依恋情结这个坏味道,说白了就是“代码放错了地方”。
解决它有两个武器:
- 搬移函数:适合简单的一对一依恋
- 策略模式:适合复杂的多对多依恋
记住一个原则:让数据和处理数据的逻辑待在一起。这样代码才会干净、好维护。
嗯,今天就聊到这里。下次你看到某个方法里全是别的类的 getter,别犹豫,动手重构吧。