大型重构(下):分离领域与表现、提炼继承体系
好,咱们接着聊大型重构。上一章我们讲了如何拆解那些“神类”,这一章要面对的是更棘手的问题——当业务逻辑和界面代码搅在一起,或者类的职责边界模糊不清时,该怎么下手。
我个人习惯把这类重构称为“动手术”。不是小修小补,而是要把整个模块的骨架重新搭一遍。别怕,虽然听起来吓人,但每一步都有章可循。
分离领域与表现:让业务逻辑“干净”起来
先说说最常见的病根:领域逻辑和表现逻辑混在一起。
我在项目中遇到过这样一个场景:一个订单处理类里,既有计算折扣的算法,又有生成HTML表格的代码,还夹杂着弹窗提示的逻辑。你想想看,这样的类改起来有多痛苦?产品经理说“折扣规则要改”,你得从一堆HTML标签里把算法扒出来;设计师说“表格样式要调”,你又得小心别碰坏折扣计算。
说白了,这就是典型的“领域与表现未分离”。
怎么治?核心思路就一句话:让领域对象只关心业务规则,让表现层只负责展示和交互。
分离的关键步骤:
- 识别出所有与UI相关的代码(HTML生成、事件处理、DOM操作)
- 把这些代码抽到表现层类中
- 领域层只保留纯业务逻辑,不依赖任何UI框架
- 通过接口或观察者模式让两层通信
来看一个具体的例子。假设我们有一个Order类,它既计算总价,又负责渲染订单表格:
// 重构前:领域与表现混在一起
public class Order {
private List<OrderItem> items;
public double calculateTotal() {
return items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
}
public String renderAsHtml() {
StringBuilder sb = new StringBuilder();
sb.append("<table>");
for (OrderItem item : items) {
sb.append("<tr><td>").append(item.getName())
.append("</td><td>").append(item.getPrice())
.append("</td></tr>");
}
sb.append("</table>");
return sb.toString();
}
}
重构之后,我们把表现逻辑剥离出去:
// 重构后:领域类只关心业务
public class Order {
private List<OrderItem> items;
public double calculateTotal() {
return items.stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
}
public List<OrderItem> getItems() {
return Collections.unmodifiableList(items);
}
}
// 表现层单独处理
public class OrderRenderer {
public String renderAsHtml(Order order) {
StringBuilder sb = new StringBuilder();
sb.append("<table>");
for (OrderItem item : order.getItems()) {
sb.append("<tr><td>").append(item.getName())
.append("</td><td>").append(item.getPrice())
.append("</td></tr>");
}
sb.append("</table>");
return sb.toString();
}
}
我的经验:分离之后,测试变得轻松多了。领域逻辑可以用纯单元测试覆盖,不需要启动UI框架。我曾经在一个遗留项目中这么干,测试覆盖率从15%直接飙到80%。
提炼继承体系:当类变得“不伦不类”
另一个常见问题是:一个类承担了多种“角色”。比如一个Employee类,既有全职员工的逻辑,又有兼职员工的逻辑,还混着实习生的规则。这种类往往充斥着大量的if-else或switch,每次加新角色都要改这个类。
嗯,这时候就该提炼继承体系了。
核心思路是:把不同的“角色”提炼成独立的子类,让每个子类只负责自己的行为。
提炼继承体系的步骤:
- 找出类中所有根据“类型”分支的逻辑
- 定义一个抽象基类,包含公共行为
- 为每个类型创建一个子类,实现各自特有的行为
- 用工厂方法或策略模式来创建合适的子类实例
来看一个例子。假设我们有一个Employee类,根据员工类型计算薪资:
// 重构前:一个类处理所有类型
public class Employee {
private String type; // "fulltime", "parttime", "intern"
public double calculatePay() {
switch (type) {
case "fulltime":
return baseSalary + bonus;
case "parttime":
return hourlyRate * hoursWorked;
case "intern":
return fixedStipend;
default:
throw new IllegalArgumentException("Unknown type");
}
}
}
重构后,我们提炼出继承体系:
// 重构后:每个子类只关心自己
public abstract class Employee {
public abstract double calculatePay();
}
public class FullTimeEmployee extends Employee {
private double baseSalary;
private double bonus;
@Override
public double calculatePay() {
return baseSalary + bonus;
}
}
public class PartTimeEmployee extends Employee {
private double hourlyRate;
private int hoursWorked;
@Override
public double calculatePay() {
return hourlyRate * hoursWorked;
}
}
public class InternEmployee extends Employee {
private double fixedStipend;
@Override
public double calculatePay() {
return fixedStipend;
}
}
注意:提炼继承体系不是万能的。如果类型变化频繁,或者类型之间共享大量逻辑,用策略模式或组合可能更合适。我曾经在一个项目中过度使用继承,结果子类数量膨胀到20多个,维护起来比原来还痛苦。
两种重构的关系
你可能会问:分离领域与表现、提炼继承体系,这两者有什么关系?
其实它们经常一起出现。当你分离领域与表现时,领域层内部可能还需要进一步提炼继承体系。反过来,提炼继承体系时,也要确保每个子类不混入表现逻辑。
我画了一张图,帮你理清它们的关系:
从图中可以看到,重构的路径是:先把“神类”拆成领域层和表现层,然后在领域层内部进一步提炼继承体系。这样每个子类都保持“干净”,只做自己该做的事。
避坑指南
最后,分享几个我踩过的坑:
我曾经犯过的错:
- 一次性重构太多:试图同时分离领域与表现、提炼继承体系、引入设计模式。结果改到一半,代码跑不起来了。建议分步走,每步都保证测试通过。
- 忽略测试:大型重构没有测试护航,就像走钢丝没有安全网。我建议先写“夹板测试”——把现有行为用测试固定下来,再动手重构。
- 过度设计:不是所有类都需要继承体系。如果只有两三种变化,用简单的条件判断可能更清晰。记住:重构的目标是让代码更容易理解,而不是更“高级”。
好了,这一章的内容就到这里。分离领域与表现、提炼继承体系,这两招用好了,你的代码会变得格外清爽。下次遇到那些“什么都干”的类,不妨试试看。