54、代码坏味道:内幕交易 - 实战搬移函数与减少耦合
内幕交易,这名字听着像金融犯罪,对吧?但在代码世界里,它同样是个大坑。说白了,就是两个模块之间,私下里搞了太多小动作——你直接读我的私有字段,我直接调你的内部方法。表面上它们各自独立,实际上早就勾搭在一起了。
我在项目中遇到过好几次这种场景。最典型的一次,是一个订单模块和一个用户模块,订单里居然直接引用了用户模块的内部缓存对象。当时我一看就头大——这要是哪天缓存策略改了,订单模块也得跟着崩。嗯,这就是典型的内幕交易。
什么是内幕交易?
内幕交易的核心特征就一条:一个类过度依赖另一个类的内部实现细节。具体表现有这些:
- 直接访问另一个类的私有或受保护字段
- 调用另一个类本应内部使用的方法
- 两个类之间频繁交换数据,形成紧密耦合
- 一个类的修改,总是导致另一个类也跟着改
判断标准:如果你改A类时,总得同时改B类,那它们之间很可能存在内幕交易。
一个真实的坏味道案例
来看一段我重构前的代码。这是一个订单处理系统,订单类直接访问了用户类的内部数据:
// 用户类
public class User {
private String name;
private String email;
private String phone;
private Map<String, String> preferences;
// 本应是内部使用的缓存
public Map<String, String> getPreferences() {
return preferences;
}
}
// 订单类 - 存在内幕交易
public class Order {
private User user;
private double amount;
public void sendNotification() {
// 直接访问 User 的内部缓存
Map<String, String> prefs = user.getPreferences();
String channel = prefs.getOrDefault("notification_channel", "email");
if ("email".equals(channel)) {
// 直接使用 User 的 email 字段
sendEmail(user.getEmail(), "您的订单金额:" + amount);
} else if ("sms".equals(channel)) {
sendSms(user.getPhone(), "您的订单金额:" + amount);
}
}
}
你想想看,这里的问题在哪?Order 类不仅知道了 User 有 preferences 这个缓存,还知道里面存了什么 key,甚至直接用了 User 的 email 和 phone 字段。这就像你家邻居直接拿钥匙开你家门拿东西——虽然关系好,但规矩不能坏。
重构策略:搬移函数与减少耦合
对付内幕交易,我个人的习惯是三步走:识别、搬移、封装。说白了,就是把不该暴露的藏起来,把该属于谁的功能还给谁。
第一步:识别内幕交易点
先找出所有跨类访问内部细节的地方。上面例子中,Order 类做了三件越界的事:
- 调用了 User.getPreferences() —— 这本应是内部方法
- 直接读取 preferences 中的 key —— 暴露了缓存结构
- 直接使用 User 的 email 和 phone 字段 —— 破坏了封装
第二步:搬移函数
把通知逻辑搬移到 User 类中。因为「如何通知用户」这件事,本质上属于用户自己的职责:
// 重构后的 User 类
public class User {
private String name;
private String email;
private String phone;
private Map<String, String> preferences;
// 将通知逻辑搬移到这里
public void sendNotification(String message) {
String channel = getNotificationChannel();
if ("email".equals(channel)) {
sendEmail(email, message);
} else if ("sms".equals(channel)) {
sendSms(phone, message);
}
}
private String getNotificationChannel() {
return preferences.getOrDefault("notification_channel", "email");
}
private void sendEmail(String to, String msg) {
// 发送邮件逻辑
}
private void sendSms(String to, String msg) {
// 发送短信逻辑
}
}
// 重构后的 Order 类 - 干净多了
public class Order {
private User user;
private double amount;
public void sendNotification() {
user.sendNotification("您的订单金额:" + amount);
}
}
我的经验:搬移函数时,别急着一步到位。先看看被搬移的函数是否依赖其他类的内部状态。如果依赖太多,可能需要先做「提取方法」或「引入参数对象」。
第三步:减少耦合
搬移之后,Order 和 User 之间的耦合度大大降低了。但还不够。我建议再加一道防线:引入中间层或接口。
// 定义通知接口
public interface Notifiable {
void sendNotification(String message);
}
// User 实现接口
public class User implements Notifiable {
// ... 同上
}
// Order 只依赖接口
public class Order {
private Notifiable notifiable;
private double amount;
public Order(Notifiable notifiable) {
this.notifiable = notifiable;
}
public void sendNotification() {
notifiable.sendNotification("您的订单金额:" + amount);
}
}
这样一来,Order 甚至不知道它通知的是 User 还是 Admin 还是其他什么东西。耦合度降到了最低。
内幕交易的其他常见场景
| 场景 | 坏味道表现 | 重构手法 |
|---|---|---|
| 跨模块数据传递 | A模块直接读取B模块的内部集合 | 搬移函数 + 引入查询方法 |
| 继承滥用 | 子类直接访问父类的 protected 字段 | 用组合替代继承 + 封装字段 |
| 回调地狱 | 多个类互相传递内部回调对象 | 引入事件总线或中介者模式 |
| 全局状态共享 | 多个类读写同一个全局变量 | 封装全局状态 + 依赖注入 |
避坑指南:我曾经在一个项目中,为了「快速实现功能」,让两个模块直接共享了一个 HashMap。结果三个月后,没人记得这个 HashMap 到底被哪些地方修改过。每次上线都提心吊胆。后来花了整整两天才把它拆干净。所以,别图一时方便,内幕交易的债迟早要还。
知识体系图:内幕交易重构核心逻辑
什么时候该搬,什么时候不该搬?
不是所有跨类访问都是内幕交易。我个人的判断标准是:看这个访问行为是否属于被访问类的核心职责。
- 该搬:Order 调用 User 的内部缓存来决定通知方式 —— 通知方式属于 User 的职责
- 不该搬:Order 读取 User 的 name 来生成订单标题 —— name 是 User 的公开属性,且 Order 只是使用它
说白了,判断标准就一条:这个逻辑如果放在被访问类里,是否更自然?如果答案是肯定的,那就搬。
核心原则:每个类只对自己的内部状态负责。别的类想用你的数据,得通过你提供的公开接口。别让外人翻你的抽屉。
嗯,内幕交易这个坏味道,其实挺隐蔽的。很多时候我们写代码时觉得「这样方便」,结果就埋下了耦合的种子。但只要你养成「先问职责归属」的习惯,就能在第一时间发现并解决它。记住:搬移函数不是目的,减少耦合才是。
公众号:蓝海资料掘金营,微信deep3321