领域驱动设计(DDD)【13】之重构中的坏味道:深入理解依恋特性(Feature Envy)与表意接口模式
文章目录
- 引言:什么是代码的"坏味道"?
- 一 初识依恋特性(Feature Envy)
- 1.1 依恋特性
- 1.2 直观例子
- 二 识别依恋特性
- 2.1 常见症状
- 2.2 检测方法
- 三 表意接口模式简介
- 3.1 表意接口(Intention-Revealing Interface)
- 3.2 表意接口示例
- 四 依恋特性与表意接口的关系
- 4.1 为什么表意接口能解决依恋特性?
- 4.2 重构示例
- 五 重构依恋特性的具体方法
- 5.1 移动方法(Move Method)
- 5.2 提取方法+移动方法
- 5.3 引入参数对象
- 六 表意接口的高级应用
- 6.1 领域驱动设计中的表意接口
- 6.2 流畅接口(Fluent Interface)
- 6.3 命令查询分离(CQS)
- 七 实际案例分析
- 7.1 电商系统案例
- 八 总结与实践
- 8.1 关键要点
- 8.2 检查清单
- 8.3 改进建议
- 8.4 避免过度优化
引言:什么是代码的"坏味道"?
- 在软件开发中,“代码坏味道"是指代码中可能存在问题或需要改进的结构特征。就像食物变质会发出异味一样,代码中的不良设计也会通过某些"症状"表现出来。Martin Fowler在《重构:改善既有代码的设计》一书中系统性地总结了这些坏味道,其中"依恋特性”(Feature Envy)是常见且值得特别关注的一种。
一 初识依恋特性(Feature Envy)
1.1 依恋特性
- 依恋特性是指一个类的方法过度使用另一个类的数据或方法,而对自身类的数据使用较少。换句话说,这个方法似乎更"喜欢"另一个类,而不是它所属的类。
1.2 直观例子
// 坏味道示例:Customer类中的方法过度使用Order类的数据
public class Customer {private String name;private String address;public String getOrderSummary(Order order) {return "Order #" + order.getOrderNumber() + " Total: $" + order.getTotalPrice() + " Items: " + order.getItemCount();}
}
- 在这个例子中,
getOrderSummary
方法几乎完全依赖于Order
类的数据,而很少使用Customer
类自身的数据,这就是典型的依恋特性。
依恋特性会导致:
- 代码组织混乱:方法放在错误的类中
- 维护困难:当被依赖类变化时,需要修改多个类
- 低内聚高耦合:类之间的依赖关系不合理
二 识别依恋特性
2.1 常见症状
- 一个方法中大量使用另一个类的
getter
方法 - 方法参数中频繁出现某个特定类的对象
- 方法名中包含另一个类的名称(如
printOrderDetails
在Customer
类中)
2.2 检测方法
- 代码审查:观察方法中对其他类的调用频率
- IDE工具:现代IDE可以显示方法对其他类的依赖
三 表意接口模式简介
3.1 表意接口(Intention-Revealing Interface)
- 表意接口是指通过接口名称和方法名称清晰表达其意图和功能的设计原则。它强调:命名应该明确表达做什么,而不是怎么做、接口应该反映业务概念而非技术细节、方法调用应该读起来像自然语言。
3.2 表意接口示例
// 不好的命名
public interface Order {void process();
}// 表意接口
public interface Order {void calculateTotalWithTax();void applyDiscount(Coupon coupon);void confirmPayment();
}
四 依恋特性与表意接口的关系
4.1 为什么表意接口能解决依恋特性?
当发现依恋特性时,通常意味着:
- 某些行为被放在了错误的类中
- 类的职责划分不清晰
- 接口没有准确反映业务意图
通过应用表意接口原则重构,我们可以:
- 将方法移动到它真正所属的类
- 创建更符合业务概念的接口
- 使代码结构更符合领域模型
4.2 重构示例
- 重构前(有依恋特性):
public class CustomerService {public String formatOrderDetails(Order order) {return String.format("Order #%s - Total: $%.2f - %d items", order.getId(), order.getTotal(), order.getItemCount());}
}
- 重构后(应用表意接口):
public class Order {private String id;private double total;private int itemCount;// 将方法移动到它真正所属的类public String getFormattedDetails() {return String.format("Order #%s - Total: $%.2f - %d items", id, total, itemCount);}
}// CustomerService现在只需调用Order的表意接口
public class CustomerService {public void displayOrder(Order order) {System.out.println(order.getFormattedDetails());}
}
五 重构依恋特性的具体方法
5.1 移动方法(Move Method)
- 适用场景:方法明显更适合放在另一个类中
步骤:
- 检查方法中使用哪个类的数据更多
- 在目标类中创建相同的方法
- 将原方法代码移动到新方法
- 调整引用,删除原方法
5.2 提取方法+移动方法
- 适用场景:方法中只有部分代码表现出依恋特性
// 重构前
public class Report {public void printEmployeeDetails(Employee emp) {// 使用Report类的数据String header = "Report: " + this.title;// 这部分过度使用Employee数据 → 依恋特性String empInfo = "Name: " + emp.getName() + ", Dept: " + emp.getDepartment() + ", Salary: " + emp.getSalary();System.out.println(header + "\n" + empInfo);}
}// 重构后
public class Employee {// 将依恋部分提取到Employee类public String getFormattedInfo() {return "Name: " + this.name + ", Dept: " + this.department + ", Salary: " + this.salary;}
}public class Report {public void printEmployeeDetails(Employee emp) {String header = "Report: " + this.title;System.out.println(header + "\n" + emp.getFormattedInfo());}
}
5.3 引入参数对象
- 适用场景:方法需要多个来自同一类的数据
// 重构前
public class Invoice {public double calculateDiscount(Customer customer) {return customer.getLoyaltyPoints() * 0.01 + (customer.isPremium() ? 0.1 : 0);}
}// 重构后
public class Customer {public double calculateDiscount() {return this.loyaltyPoints * 0.01 + (this.isPremium ? 0.1 : 0);}
}public class Invoice {public double applyDiscount(Customer customer) {return customer.calculateDiscount();}
}
六 表意接口的高级应用
6.1 领域驱动设计中的表意接口
在DDD中,表意接口表现为:
- 领域模型的清晰表达
- 聚合根提供业务意义的接口
- 领域服务的明确职责
6.2 流畅接口(Fluent Interface)
- 流畅接口是表意接口的一种扩展,使代码读起来更自然:
// 传统方式
order.setCustomer(customer);
order.setDate(today);
order.addItem(item1);
order.addItem(item2);// 流畅接口
order.forCustomer(customer).withDate(today).including(item1).including(item2);
6.3 命令查询分离(CQS)
表意接口也体现在命令查询分离原则中:
- 命令方法:执行操作,改变状态(命名应为动词:
saveOrder()
) - 查询方法:返回数据,不改变状态(命名应为名词:
getOrderTotal()
)
七 实际案例分析
7.1 电商系统案例
- 问题代码:
public class ShoppingCartUI {public void displayCart(ShoppingCart cart) {// 计算总价double total = 0;for (Item item : cart.getItems()) {total += item.getPrice() * item.getQuantity();}// 应用折扣if (cart.getCustomer().isPremium()) {total *= 0.9;}// 显示逻辑System.out.println("Your cart total: $" + total);}
}
问题识别:
displayCart
方法中大量计算逻辑都依赖于ShoppingCart
和Item
的数据- UI类承担了过多的业务逻辑
- 折扣计算分散在多处
- 重构步骤:
- 将计算逻辑移动到ShoppingCart类:
public class ShoppingCart {public double calculateTotal() {double total = 0;for (Item item : this.items) {total += item.getPrice() * item.getQuantity();}return total;}public double applyDiscounts() {double total = calculateTotal();if (this.customer.isPremium()) {total *= 0.9;}return total;}
}
- 简化UI类:
public class ShoppingCartUI {public void displayCart(ShoppingCart cart) {System.out.println("Your cart total: $" + cart.applyDiscounts());}
}
- 进一步优化为表意接口:
public class ShoppingCart {// 更表意的接口名称public double getDiscountedTotal() {return applyDiscountStrategy(calculateSubtotal());}private double calculateSubtotal() { return items.stream().mapToDouble(item -> item.getPrice() * item.getQuantity()).sum();}private double applyDiscountStrategy(double subtotal) {return sutotal*0.9;}
}
八 总结与实践
8.1 关键要点
- 依恋特性表现为方法"过度关心"其他类的数据。
- 表意接口通过明确表达意图来自然避免依恋特性。
- 重构方法包括移动方法、提取方法等。
- 好的设计应该反映业务领域,而不仅是技术考量。
8.2 检查清单
- 当你编写或审查代码时,可以问这些问题:
- 这个方法主要操作哪个类的数据?
- 方法名是否准确反映了它的职责?
- 如果把这个方法移到另一个类,代码会更清晰吗?
- 其他开发者能否从接口名称理解其功能?
8.3 改进建议
- 定期代码审查:寻找依恋特性坏味道
- 结对编程:第二双眼睛更容易发现问题
- 学习领域驱动设计:更好的领域建模自然减少依恋特性
- 重构小步前进:每次解决一个小问题,避免大规模重写
8.4 避免过度优化
- 虽然依恋特性是一个需要关注的坏味道,但也要注意:
- 不要盲目移动所有方法:有时方法确实需要访问多个类的数据
- 考虑领域逻辑:业务逻辑应该决定代码结构,而不是单纯追求技术上的"纯净"
- 权衡设计复杂度:简单的依恋有时比复杂的间接设计更好