Java 设计模式实战:工厂与策略模式落地

在 Java 项目从单体走向多人协作的过程中,Java 设计模式往往是代码可维护性的分水岭。很多团队不是不会写业务,而是对象创建散落各处、条件分支层层嵌套,导致一次需求变更要改动十几个类,回归测试范围被无限放大。本文聚焦两个高频且易落地的模式——工厂模式与策略模式,用可直接复制的生产级代码,讲清它们各自解决什么问题、何时该用、又该如何组合。吃透这两个,你就掌握了”创建”与”行为”两套解耦思路,足以应付日常开发中八成以上的抽象需求。

一、工厂模式:把”new”集中起来

当一类对象的创建逻辑依赖外部配置或环境变量时,散落的 new 会让调用方被迫了解实现细节。更隐蔽的问题是,调用方为了创建一个对象,必须先 import 一堆具体实现类,编译期耦合让模块根本拆不开。工厂模式的核心价值,是把”创建什么”的决策上收到一个地方,让上层只关心”用它”而非”怎么造它”。

1.1 简单工厂的局限

简单工厂用一个 if-else 返回不同子类,但当类型增多,这个类会膨胀成”上帝方法”,每加一种产品都要改它,直接违反开闭原则。更稳妥的做法是升级为工厂方法,让扩展只发生在新增类上,老代码一行都不用动。

1.2 工厂方法实战

下面以”通知发送器”为例:短信、邮件、站内信都实现同一接口,由各自的工厂负责创建,调用方只依赖工厂接口,对具体实现一无所知。

// 统一产品接口
public interface Notifier {
    void send(String message);
}

// 具体产品
public class SmsNotifier implements Notifier {
    public void send(String message) {
        System.out.println("SMS: " + message);
    }
}

// 抽象工厂
public interface NotifierFactory {
    Notifier create();
}

// 具体工厂
public class SmsNotifierFactory implements NotifierFactory {
    public Notifier create() { return new SmsNotifier(); }
}

// 调用方只依赖工厂,不关心具体实现
NotifierFactory factory = new SmsNotifierFactory();
Notifier notifier = factory.create();
notifier.send("订单已发货");

1.3 何时才该上工厂

工厂不是免费午餐。出现以下信号时再引入:对象创建需要读取配置(如从 yml 读渠道类型)、构造过程复杂(要拼装多个依赖或做参数校验)、同一接口有多种实现且会持续增加。反之,创建一个无状态、无依赖的普通对象,直接 new 反而最清晰——不要为了”显得专业”而套工厂。

二、策略模式:让算法可插拔

策略模式解决的是”同一行为有多种算法、且需要运行时切换”的问题。它与工厂模式常被一起使用:工厂负责造出策略对象,策略负责执行具体行为,二者职责分明。需要提醒的是,策略模式最容易被误用的是”为了用而用”。它的前提是有至少两种可互换算法,且调用方不关心选了哪一个;如果只有一种实现,一个普通方法就够,没必要先定义接口再写实现类,那只是把简单问题包装成框架。

2.1 定义策略接口

以”价格优惠计算”为例,满减、折扣、拼团是三种可以互相替换的算法,统一抽象成策略接口,调用方面向接口编程。

public interface DiscountStrategy {
    BigDecimal calc(BigDecimal price);
}

public class FullReductionStrategy implements DiscountStrategy {
    public BigDecimal calc(BigDecimal price) {
        return price.compareTo(new BigDecimal("200")) >= 0
                ? price.subtract(new BigDecimal("30")) : price;
    }
}

// 上下文:持有策略,运行时注入
public class PriceContext {
    private DiscountStrategy strategy;
    public void setStrategy(DiscountStrategy s) { this.strategy = s; }
    public BigDecimal finalPrice(BigDecimal price) {
        return strategy.calc(price);
    }
}

2.2 与 Spring 结合

在 Spring 项目中,可把全部策略以 Map<String, DiscountStrategy> 形式自动注入,按业务码路由,彻底干掉 switch 满天飞的分支,新增算法只需加一个 @Component 类。

@Component
public class StrategyRouter {
    @Autowired
    private Map strategies;

    public BigDecimal apply(String code, BigDecimal price) {
        DiscountStrategy s = strategies.get(code);
        if (s == null) throw new IllegalArgumentException("未知策略:" + code);
        return s.calc(price);
    }
}

2.3 与工厂联手:一个完整闭环

以”下单优惠”接口为例:工厂根据配置中心下发的 strategyCode 造出对应 DiscountStrategyPriceContext 注入后计算。配置热更新时,只需工厂返回新策略,上下文与业务代码零改动。这种”创建归工厂、执行归策略”的分工,让灰度、回滚、A/B 都只发生在边界处,主流程始终稳定。在高并发读多写少的接口中,用策略模式切换不同的 Redis 缓存方案,也是同样的思路,既能灰度又能回滚,比直接改主流程安全得多。

三、工厂模式 vs 策略模式:一张表看懂

维度工厂模式策略模式
关注点对象如何创建行为如何执行
变化时机类型增减时改工厂算法替换时扩展
典型场景多数据源、多通道通知优惠计算、校验规则
协作关系常作为策略的”制造者”被工厂造出后注入上下文

四、重构信号:你的代码在求救

设计模式是”闻到坏味道才用”,不是”先想着用”。出现以下信号,就是工厂与策略登场的时刻:

  • 类型分支反复出现:同一个 if (type.equals("sms")) 在发送、日志、统计里各写一遍,改一处要全局搜。
  • 一个类里十几个 new:构造逻辑散落,单测要 mock 一长串依赖才能跑起来。
  • 新增一种类型要改 N 处:每加一个渠道,从工厂到调用方全都要动,说明创建没收口。
  • 测试困难:因为行为被写死在主流程,无法在不启真实依赖的情况下验证不同分支。

下面这段就是典型坏味道——用策略 + 工厂改完之后,新增算法只需加一个类,主流程一行不动:

// 坏味道:行为写死在主流程
public BigDecimal checkout(String type, BigDecimal price) {
    if ("full".equals(type)) { /* 满减逻辑 */ }
    else if ("discount".equals(type)) { /* 折扣逻辑 */ }
    else if ("group".equals(type)) { /* 拼团逻辑 */ }
    return price; // 每加一种都要改这里
}

// 改后:策略 + 工厂,主流程零改动
public BigDecimal checkout(DiscountStrategy s, BigDecimal price) {
    return s.calc(price);
}

五、落地时的三个注意点

1. 不要为”可能用到”而设计。 只有一个实现时,直接 new 更简单,过度抽象反而增加阅读与维护成本,也让新人理解门槛变高。

2. 命名要暴露意图。 AbstractHandlerFactoryManager 这种名字会逼死同事,宁可叫 OrderNotifierFactory,一眼看懂职责,后期接手的人会感谢你。

3. 策略枚举化。 固定几种算法时,用 enum 实现接口比建一堆类更轻量,编译期就能发现遗漏的分支,避免运行时才爆出”未知策略”。

设计模式不是炫技,而是把”变化点”提前隔离的工程纪律。当你发现自己在不停复制粘贴 if (type.equals("sms")) 这类分支时,就是工厂与策略模式登场的时候。先把”创建”和”行为”这两件事拆干净,后续的需求变更会轻松一个量级,代码也会从”能跑”走向”好改”。

上一篇 技术选型决策实战:在相似方案间做出可靠选择
下一篇 Cilium 实战:用 eBPF 数据面替代 kube-proxy 踩坑记