Java 迪米特法则实战:3个典型代码坏味道与重构方案对比

1. 钱包暴露模式:隐私保护的边界

在超市结账场景中,我们经常看到这样的代码实现:

// 反例:钱包直接暴露
class Customer {
    private Wallet wallet = new Wallet(100f);
    
    public Wallet getWallet() {
        return wallet;
    }
}

class Cashier {
    public void charge(Customer customer, float amount) {
        Wallet wallet = customer.getWallet();
        if(wallet.getBalance() >= amount) {
            wallet.deduct(amount);
        }
    }
}

这种实现存在三个典型问题:

  1. 收银员直接操作顾客钱包内部状态
  2. 余额检查逻辑暴露在外部
  3. 钱包实现细节与收银逻辑耦合

重构方案 采用信息隐藏原则:

// 正例:封装钱包操作
class Customer {
    private Wallet wallet = new Wallet(100f);
    
    public boolean pay(float amount) {
        return wallet.tryDeduct(amount);
    }
}

class Cashier {
    public void charge(Customer customer, float amount) {
        if(customer.pay(amount)) {
            // 支付成功处理
        }
    }
}

关键改进点对比:

维度 原始方案 重构方案
耦合度 收银员与钱包直接耦合 仅与顾客对象交互
信息暴露 钱包余额和扣款逻辑完全暴露 仅暴露支付能力
变更影响 修改钱包逻辑需改动收银代码 钱包修改不影响收银流程

2. 跨层调用陷阱:组织架构的边界

学校管理系统常出现这种层级混乱的情况:

// 反例:跨层调用
class SchoolManager {
    void printAllEmployees(CollegeManager manager) {
        // 直接访问学院员工细节
        List<CollegeEmployee> collegeEmps = manager.getAllEmployees();
        
        // 打印学院员工
        for(CollegeEmployee emp : collegeEmps) {
            System.out.println(emp.getDetail());
        }
        
        // 打印校本部员工
        printSchoolEmployees();
    }
}

这种实现违反了迪米特法则的两个要点:

  1. 校本部管理者直接依赖学院员工实现类
  2. 学院员工细节跨越组织层级暴露

重构方案 采用信息中介模式:

// 正例:层级封装
class CollegeManager {
    public void printEmployees() {
        List<CollegeEmployee> emps = getAllEmployees();
        // 打印逻辑封装在学院内部
    }
}

class SchoolManager {
    void printAllEmployees(CollegeManager manager) {
        manager.printEmployees();  // 委托学院自行打印
        printSchoolEmployees();
    }
}

重构前后UML对比:

原始结构:
SchoolManager -> CollegeManager -> CollegeEmployee

优化结构:
SchoolManager -> CollegeManager
CollegeManager -> CollegeEmployee

3. 方法链污染:通信距离的边界

订单处理系统中常见的方法链调用:

// 反例:过长的方法链
class OrderService {
    public void processOrder(Order order) {
        String trackingNo = order.getCustomer()
                               .getAddress()
                               .getShippingService()
                               .createTrackingNumber();
        // 使用trackingNo...
    }
}

这种方法链存在三个隐患:

  1. 订单服务需要了解客户、地址、物流等多个领域知识
  2. 任何中间环节变更都会影响订单处理
  3. 调试时需要追踪整个调用链路

重构方案 采用委托模式:

// 正例:限制通信距离
class Customer {
    public String getShippingTrackingNo() {
        return address.requestTrackingNumber();
    }
}

class OrderService {
    public void processOrder(Order order) {
        String trackingNo = order.getCustomer()
                               .getShippingTrackingNo();
        // 使用trackingNo...
    }
}

关键指标对比表:

指标 方法链方案 委托方案
耦合对象数量 4个 2个
变更影响范围 广泛 局部
单元测试难度
业务语义表达 模糊 清晰

4. 重构决策流程图

针对迪米特法则的应用,我们总结出以下决策流程:

  1. 识别关系类型

    • 成员变量 → 直接朋友
    • 方法参数 → 直接朋友
    • 局部变量 → 潜在陌生人
  2. 评估交互距离

    graph TD
    A[当前类] --> B{交互对象}
    B -->|直接朋友| C[允许通信]
    B -->|陌生人| D[考虑重构]
    
  3. 选择重构策略

    • 中间人模式:引入中介对象
    • 委托模式:转移职责
    • 接口隔离:定义最小契约
  4. 验证重构效果

    • 类图箭头减少
    • 方法参数列表缩短
    • 局部变量中的陌生类消失

实际项目中,一个典型的代码坏味道检查清单如下:

  • [ ] 一个方法内出现三个以上的点操作符(.)
  • [ ] 需要导入不直接相关的类
  • [ ] 方法参数包含中间对象
  • [ ] 单元测试需要mock多层依赖

5. 经验总结与实用技巧

在金融系统重构实践中,我们发现这些典型场景:

  1. DTO转换器陷阱

    // 反例
    public AccountDTO convert(Account account) {
        UserDTO user = new UserDTO();
        user.setName(account.getUser().getName());
        // 暴露用户细节...
    }
    
    // 正例
    public AccountDTO convert(Account account) {
        return account.toDTO(); // 封装转换逻辑
    }
    
  2. 服务编排过度暴露

    // 反例
    class OrderFacade {
        public void placeOrder(Order order) {
            inventoryService.checkStock(order.getItems());
            // 直接调用多个服务细节
        }
    }
    
    // 正例
    class OrderService {
        public void placeOrder(Order order) {
            order.validate(); // 聚合根封装规则
        }
    }
    
  3. 性能优化时的平衡 在需要性能优化的场景,可以适度放宽迪米特法则:

    // 妥协方案:添加文档说明
    @PerformanceCritical
    @ViolatesDemeterJustification("避免多次查询的性能损耗")
    public Result process(Batch batch) {
        // 直接访问batch内部细节
    }
    

对于遗留系统改造,建议采用渐进式重构:

  1. 先识别最严重的"聊天狂"类
  2. 使用包装模式创建中间层
  3. 逐步将行为迁移到合适的位置
  4. 最后移除不必要的getter方法

在微服务架构中,迪米特法则同样适用:

  • 服务间应通过明确的API契约交互
  • 避免服务直接操作其他服务的数据库模型
  • DTO应该保持最小信息原则
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐