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 类做了三件越界的事:

  1. 调用了 User.getPreferences() —— 这本应是内部方法
  2. 直接读取 preferences 中的 key —— 暴露了缓存结构
  3. 直接使用 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