大型重构(下):分离领域与表现、提炼继承体系

好,咱们接着聊大型重构。上一章我们讲了如何拆解那些“神类”,这一章要面对的是更棘手的问题——当业务逻辑和界面代码搅在一起,或者类的职责边界模糊不清时,该怎么下手。

我个人习惯把这类重构称为“动手术”。不是小修小补,而是要把整个模块的骨架重新搭一遍。别怕,虽然听起来吓人,但每一步都有章可循。

分离领域与表现:让业务逻辑“干净”起来

先说说最常见的病根:领域逻辑和表现逻辑混在一起。

我在项目中遇到过这样一个场景:一个订单处理类里,既有计算折扣的算法,又有生成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-elseswitch,每次加新角色都要改这个类。

嗯,这时候就该提炼继承体系了。

核心思路是:把不同的“角色”提炼成独立的子类,让每个子类只负责自己的行为

提炼继承体系的步骤:

  1. 找出类中所有根据“类型”分支的逻辑
  2. 定义一个抽象基类,包含公共行为
  3. 为每个类型创建一个子类,实现各自特有的行为
  4. 用工厂方法或策略模式来创建合适的子类实例

来看一个例子。假设我们有一个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多个,维护起来比原来还痛苦。

两种重构的关系

你可能会问:分离领域与表现、提炼继承体系,这两者有什么关系?

其实它们经常一起出现。当你分离领域与表现时,领域层内部可能还需要进一步提炼继承体系。反过来,提炼继承体系时,也要确保每个子类不混入表现逻辑。

我画了一张图,帮你理清它们的关系:

大型重构:分离领域与表现 + 提炼继承体系 原始“神类” 分离领域与表现 领域层(纯业务逻辑) 表现层(UI/展示) 提炼继承体系 子类A(全职) 子类B(兼职) 子类C(实习) 渲染器 / 控制器 事件处理 / 数据绑定 通过接口通信

从图中可以看到,重构的路径是:先把“神类”拆成领域层和表现层,然后在领域层内部进一步提炼继承体系。这样每个子类都保持“干净”,只做自己该做的事。

避坑指南

最后,分享几个我踩过的坑:

我曾经犯过的错:

  • 一次性重构太多:试图同时分离领域与表现、提炼继承体系、引入设计模式。结果改到一半,代码跑不起来了。建议分步走,每步都保证测试通过。
  • 忽略测试:大型重构没有测试护航,就像走钢丝没有安全网。我建议先写“夹板测试”——把现有行为用测试固定下来,再动手重构。
  • 过度设计:不是所有类都需要继承体系。如果只有两三种变化,用简单的条件判断可能更清晰。记住:重构的目标是让代码更容易理解,而不是更“高级”。

好了,这一章的内容就到这里。分离领域与表现、提炼继承体系,这两招用好了,你的代码会变得格外清爽。下次遇到那些“什么都干”的类,不妨试试看。


公众号:蓝海资料掘金营,微信deep3321