Java 迪米特法则实战:3个典型代码坏味道与重构方案对比
·
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);
}
}
}
这种实现存在三个典型问题:
- 收银员直接操作顾客钱包内部状态
- 余额检查逻辑暴露在外部
- 钱包实现细节与收银逻辑耦合
重构方案 采用信息隐藏原则:
// 正例:封装钱包操作
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();
}
}
这种实现违反了迪米特法则的两个要点:
- 校本部管理者直接依赖学院员工实现类
- 学院员工细节跨越组织层级暴露
重构方案 采用信息中介模式:
// 正例:层级封装
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...
}
}
这种方法链存在三个隐患:
- 订单服务需要了解客户、地址、物流等多个领域知识
- 任何中间环节变更都会影响订单处理
- 调试时需要追踪整个调用链路
重构方案 采用委托模式:
// 正例:限制通信距离
class Customer {
public String getShippingTrackingNo() {
return address.requestTrackingNumber();
}
}
class OrderService {
public void processOrder(Order order) {
String trackingNo = order.getCustomer()
.getShippingTrackingNo();
// 使用trackingNo...
}
}
关键指标对比表:
| 指标 | 方法链方案 | 委托方案 |
|---|---|---|
| 耦合对象数量 | 4个 | 2个 |
| 变更影响范围 | 广泛 | 局部 |
| 单元测试难度 | 高 | 低 |
| 业务语义表达 | 模糊 | 清晰 |
4. 重构决策流程图
针对迪米特法则的应用,我们总结出以下决策流程:
-
识别关系类型
- 成员变量 → 直接朋友
- 方法参数 → 直接朋友
- 局部变量 → 潜在陌生人
-
评估交互距离
graph TD A[当前类] --> B{交互对象} B -->|直接朋友| C[允许通信] B -->|陌生人| D[考虑重构] -
选择重构策略
- 中间人模式:引入中介对象
- 委托模式:转移职责
- 接口隔离:定义最小契约
-
验证重构效果
- 类图箭头减少
- 方法参数列表缩短
- 局部变量中的陌生类消失
实际项目中,一个典型的代码坏味道检查清单如下:
- [ ] 一个方法内出现三个以上的点操作符(.)
- [ ] 需要导入不直接相关的类
- [ ] 方法参数包含中间对象
- [ ] 单元测试需要mock多层依赖
5. 经验总结与实用技巧
在金融系统重构实践中,我们发现这些典型场景:
-
DTO转换器陷阱
// 反例 public AccountDTO convert(Account account) { UserDTO user = new UserDTO(); user.setName(account.getUser().getName()); // 暴露用户细节... } // 正例 public AccountDTO convert(Account account) { return account.toDTO(); // 封装转换逻辑 } -
服务编排过度暴露
// 反例 class OrderFacade { public void placeOrder(Order order) { inventoryService.checkStock(order.getItems()); // 直接调用多个服务细节 } } // 正例 class OrderService { public void placeOrder(Order order) { order.validate(); // 聚合根封装规则 } } -
性能优化时的平衡 在需要性能优化的场景,可以适度放宽迪米特法则:
// 妥协方案:添加文档说明 @PerformanceCritical @ViolatesDemeterJustification("避免多次查询的性能损耗") public Result process(Batch batch) { // 直接访问batch内部细节 }
对于遗留系统改造,建议采用渐进式重构:
- 先识别最严重的"聊天狂"类
- 使用包装模式创建中间层
- 逐步将行为迁移到合适的位置
- 最后移除不必要的getter方法
在微服务架构中,迪米特法则同样适用:
- 服务间应通过明确的API契约交互
- 避免服务直接操作其他服务的数据库模型
- DTO应该保持最小信息原则
更多推荐



所有评论(0)