一、业务堆出的“上帝类”坑

写Java代码久了,难免会遇到那种动辄几千行的“上帝类”——把各种业务逻辑全塞在一个类里,改一行代码要翻遍几十行if-else,新增个功能还要在原来的分支里再加判断,稍微改坏一个地方,全链路测试都要跑一遍,稍不注意就出线上bug。最典型的场景就是处理不同渠道的订单:京东、淘宝、拼多多,每个渠道的校验规则、物流接口、通知方式都不一样,一开始图省事把所有逻辑堆在一个类里,久而久之就成了没人敢碰的“黑盒”。

1.1 上帝类的日常痛点

举个实际的例子:之前接手过一个电商项目的订单处理代码,一个类里有近2000行代码,光if-else分支就有7个,每个渠道的逻辑重复度低但都要写全,要改京东的短信通知方式,得在茫茫多的分支里找到对应的代码,改完还要测所有渠道的逻辑,生怕把淘宝的逻辑也改崩了。这种代码的维护成本极高,新增一个视频号渠道还要再加一个else if,整个类的行数还会涨,直到完全没法维护。

1.2 典型的上帝类代码示例

先看原始的“上帝类”代码,技术栈是Java 8:

// 技术栈:Java 8
// 这就是典型的上帝类,把所有渠道的订单处理逻辑全堆在一起
public class OrderHandler {
    // 处理不同渠道订单的统一入口
    public void handleOrder(String channel, Order order) {
        // 分支1:京东渠道逻辑
        if ("jd".equals(channel)) {
            // 京东专属校验:金额不超过1万
            if (order.getPrice() > 10000) {
                throw new RuntimeException("京东订单金额不能超过1万");
            }
            // 调用京东物流接口
            System.out.println("调用京东物流接口,订单号:" + order.getId());
            // 给京东用户发短信
            System.out.println("给京东用户发送短信,订单号:" + order.getId());
        } 
        // 分支2:淘宝渠道逻辑
        else if ("taobao".equals(channel)) {
            // 淘宝专属校验:金额不超过5千
            if (order.getPrice() > 5000) {
                throw new RuntimeException("淘宝订单金额不能超过5千");
            }
            // 调用菜鸟物流接口
            System.out.println("调用菜鸟物流接口,订单号:" + order.getId());
            // 给淘宝用户发站内信
            System.out.println("给淘宝用户发送站内信,订单号:" + order.getId());
        } 
        // 分支3:拼多多渠道逻辑
        else if ("pdd".equals(channel)) {
            // 拼多多专属校验:金额不超过3千
            if (order.getPrice() > 3000) {
                throw new RuntimeException("拼多多订单金额不能超过3千");
            }
            // 调用极兔物流接口
            System.out.println("调用极兔物流接口,订单号:" + order.getId());
            // 给拼多多用户发APP通知
            System.out.println("给拼多多用户发送APP通知,订单号:" + order.getId());
        } 
        // 后续新增渠道就要在这里再加else if,代码越来越乱
        else {
            throw new RuntimeException("不支持的渠道:" + channel);
        }
    }
}

// 订单实体类,存储订单基本信息
class Order {
    private String id;   // 订单号
    private String channel; // 渠道:jd/taobao/pdd
    private double price; // 订单金额
    // 实际开发中要补全构造方法、getter/setter,这里简化处理
    public Order(String id, String channel, double price) {
        this.id = id;
        this.channel = channel;
        this.price = price;
    }
    // 省略getter/setter,示例中用的话默认有
}

这段代码的问题很明显:每个渠道的逻辑独立,但都堆在一个类里,新增渠道要改原有代码,违反了“改原有代码会引入bug”的常见开发原则,维护难度极高。

二、两大设计模式:拆上帝类的“手术刀”

要拆解这种冗长的分支,不用大改架构,用两个常用的设计模式就能解决——策略模式+模板方法。这两个模式配合起来,既能保留原有功能的行为一致,又能把庞大的上帝类拆成多个小类,每个小类只负责一件事。

2.1 先搞懂两个模式的大白话意思

  • 策略模式:就像给每个渠道的处理方式单独做一个小工具盒子,想用哪个就拿哪个,不用把所有工具堆在一个大柜子里。比如把京东的订单处理、淘宝的订单处理、拼多多的订单处理,每个都做成一个独立的策略类,想加新渠道就加新的小盒子,不用碰原来的盒子。
  • 模板方法:就像定一个固定的做事流程,比如做奶茶要先放茶底、加奶、加糖、封杯,每个步骤是固定的,但每个步骤的具体料可以换。这里订单处理的流程是固定的:先校验规则、再调用物流、再发通知,每个渠道可以自己改每个步骤的具体内容,不用改流程顺序。

2.2 用模板方法定订单处理的固定流程

首先,所有渠道的订单处理流程都是一样的,所以我们定一个模板类,把流程固定下来,不让每个渠道自己乱改顺序:

// 技术栈:Java 8
// 模板方法抽象类,定义订单处理的固定流程,防止每个渠道乱改步骤顺序
public abstract class OrderProcessTemplate {
    // 骨架方法:用final修饰,防止子类修改流程顺序,保证所有渠道的处理逻辑顺序一致
    public final void processOrder(Order order) {
        validateRule(order); // 步骤1:校验规则,每个渠道自己实现具体校验逻辑
        callLogistics(order); // 步骤2:调用物流,每个渠道自己实现对应物流
        sendNotification(order); // 步骤3:发送通知,每个渠道自己实现通知方式
    }

    // 抽象方法,让每个渠道的策略类自己实现具体逻辑
    protected abstract void validateRule(Order order);
    protected abstract void callLogistics(Order order);
    protected abstract void sendNotification(Order order);
}

这里的关键点是,把流程的顺序定死,用final修饰骨架方法,这样不管怎么改,每个渠道的订单处理步骤都是对的,不会出现有的渠道先通知再发货的低级错误。

2.3 用策略模式封装每个渠道的分支

现在,每个渠道的处理逻辑,都做成一个独立的策略类,继承刚才的模板类,实现自己的具体逻辑。这样每个类只负责一个渠道,不会再堆在一起:

// 京东渠道的策略类,负责京东订单的处理逻辑
public class JdOrderProcess extends OrderProcessTemplate {
    // 实现京东专属的校验规则
    @Override
    protected void validateRule(Order order) {
        if (order.getPrice() > 10000) {
            throw new RuntimeException("京东订单金额不能超过1万");
        }
    }

    // 调用京东自己的物流接口
    @Override
    protected void callLogistics(Order order) {
        System.out.println("调用京东物流接口,订单号:" + order.getId());
    }

    // 给京东用户发短信通知
    @Override
    protected void sendNotification(Order order) {
        System.out.println("给京东用户发送短信,订单号:" + order.getId());
    }
}

// 淘宝渠道的策略类,逻辑只属于淘宝
public class TaobaoOrderProcess extends OrderProcessTemplate {
    @Override
    protected void validateRule(Order order) {
        if (order.getPrice() > 5000) {
            throw new RuntimeException("淘宝订单金额不能超过5千");
        }
    }

    @Override
    protected void callLogistics(Order order) {
        System.out.println("调用菜鸟物流接口,订单号:" + order.getId());
    }

    @Override
    protected void sendNotification(Order order) {
        System.out.println("给淘宝用户发送站内信,订单号:" + order.getId());
    }
}

// 拼多多渠道的策略类,逻辑只属于拼多多
public class PddOrderProcess extends OrderProcessTemplate {
    @Override
    protected void validateRule(Order order) {
        if (order.getPrice() > 3000) {
            throw new RuntimeException("拼多多订单金额不能超过3千");
        }
    }

    @Override
    protected void callLogistics(Order order) {
        System.out.println("调用极兔物流接口,订单号:" + order.getId());
    }

    @Override
    protected void sendNotification(Order order) {
        System.out.println("给拼多多用户发送APP通知,订单号:" + order.getId());
    }
}

这样的好处是,新增一个渠道,比如视频号,只需要再加一个VideoOrderProcess类,不用改任何原来的代码,符合“改原有代码越少,bug越少”的原则。

2.4 搭建统一调用入口,保持原有调用方式

重构的关键是,对外的调用方式不能变,原来的代码都是调用OrderHandler的handleOrder方法,所以我们要做一个上下文类,对外提供和原来一样的接口,内部用策略模式选择对应的策略类:

// 技术栈:Java 8
// 策略上下文:对外提供统一的处理方法,内部根据渠道选择对应的策略类,保持和原有调用方式完全一致
public class OrderProcessContext {
    // 简单工厂,根据渠道返回对应的策略实例,实际开发可以用Spring管理这些策略Bean
    private OrderProcessTemplate getStrategy(String channel) {
        switch (channel) {
            case "jd":
                return new JdOrderProcess();
            case "taobao":
                return new TaobaoOrderProcess();
            case "pdd":
                return new PddOrderProcess();
            default:
                throw new RuntimeException("不支持的渠道:" + channel);
        }
    }

    // 对外的统一方法,和原来OrderHandler的handleOrder方法签名完全一样,保持行为一致
    public void handleOrder(String channel, Order order) {
        // 根据渠道拿到对应的策略,调用模板类的processOrder方法
        OrderProcessTemplate strategy = getStrategy(channel);
        strategy.processOrder(order);
    }
}

现在,对外的调用方式和原来完全一样,比如原来的代码写的是new OrderHandler().handleOrder("jd", new Order(...)),现在只需要改成new OrderProcessContext().handleOrder("jd", new Order(...)),功能完全一样,没有任何变化。

三、重构的实践细节:不踩坑的关键

重构最怕的就是改完之后功能不对,所以要注意几个关键细节,保证重构过程不破坏原有功能。

3.1 先写单元测试,再动手重构

重构的第一原则:先把原来的所有分支的测试用例都写完,跑通,再动手改代码。比如原来的OrderHandler的handleOrder方法,针对京东、淘宝、拼多多分别写测试,每个测试都要覆盖:合法金额、超额金额、不支持的渠道这几个场景,确保重构前后的测试结果完全一样。如果原来没有测试,一定要先补全测试,这是重构的底线,不然出了问题根本不知道是原来的bug还是重构的问题。

3.2 小步迭代,不要一次性全改

不要把整个上帝类一下子拆完,分批次来:比如先把京东的逻辑改成策略类,测试通过,再改淘宝的,再改拼多多的,每次只改一个分支,测试通过再继续。这样就算改坏了,也只会影响一个渠道,不会出线上事故,而且排查问题也快。

3.3 技术优缺点的真实感受

这种重构方式的优点很明显:每个策略类职责单一,新增渠道不用改原有代码,可读性极高,调试的时候可以单独改某个渠道的逻辑,不用翻大段的if-else;缺点是会新增很多小类,对于只有2-3个分支的小项目可能有点冗余,但对于有5个以上分支、后续会新增功能的大项目,长远来看利远大于弊。

3.4 注意事项:别过度设计

不是所有的if-else都要改成策略模式,比如只有2个分支,而且后续不会新增,直接改if-else的可读性反而更好。一般当分支数超过3个,或者后续大概率会新增分支,或者单个类的代码行数超过1000行,这种情况才适合用这种重构方式,不然会造成不必要的类膨胀。

四、适用场景与总结

4.1 什么时候适合用

当你遇到以下情况,就可以考虑用这种方式重构:

  1. 一个类里有超过3个的if-else分支,每个分支的逻辑独立;
  2. 后续大概率会新增多个分支(比如新增不同渠道、不同类型的订单);
  3. 单个类的代码行数超过1000行,修改需要翻大量代码;
  4. 改某个分支的逻辑容易影响其他分支,测试成本高。

4.2 总结

遗留Java系统里的上帝类是维护的噩梦,用策略模式+模板方法的组合,既能拆解冗长的分支,又能保持原有功能的行为一致。重构时一定要先写测试,小步迭代,不要过度设计,这样可以大幅提高系统的可维护性,降低后续的维护成本。这种方式不仅适合订单处理,也适合其他有多个独立分支的业务场景,比如支付方式的处理、不同平台的登录逻辑等,都是非常实用的重构技巧。