别再写一堆 if-else 了:用策略模式重构你的业务代码

一、先来看一段"眼熟"的代码

做订单系统的时候,几乎每个人都写过类似这样的东西:

python 复制代码
def calc_discount(order, user_level):
    if user_level == "normal":
        return order.amount * 0.95
    elif user_level == "vip":
        return order.amount * 0.85
    elif user_level == "svip":
        return order.amount * 0.7
    else:
        return order.amount

刚上线时一切正常。三个月后,运营加了"新人券""满减""会员日""渠道专享价",代码变成了这样:

python 复制代码
if user_level == "normal" and has_coupon and is_member_day:
    ...
elif ...

嵌套越来越深,改一处崩两处,测试用例越堆越多,最后没人敢动这个文件。

问题的本质不在于逻辑复杂,而在于"判断"和"计算"被绑死在了一起。 每次新增规则,都必须修改同一个函数,违反了开闭原则(对扩展开放,对修改关闭)。

解法就是策略模式------它可能是所有设计模式里最容易落地、性价比最高的一个。

二、策略模式的核心思想

一句话:把每个分支抽成一个独立的类(或函数),让它们实现同一个接口,然后在运行时按需切换。

结构上只有三个角色:

角色 作用
Strategy(策略接口) 定义统一的执行方法
ConcreteStrategy(具体策略) 每个分支一个实现
Context(上下文) 持有策略引用,负责调用

听起来很学术,看代码就明白了。

三、Python 版本:用字典当注册表

Python 里甚至不需要搞一堆类,一等公民的函数加上字典就够了:

python 复制代码
from dataclasses import dataclass
from typing import Callable, Dict


@dataclass
class Order:
    amount: float
    channel: str = "app"


# 1. 每个策略就是一个普通函数
def normal_discount(order: Order) -> float:
    return order.amount * 0.95


def vip_discount(order: Order) -> float:
    return order.amount * 0.85


def svip_discount(order: Order) -> float:
    return order.amount * 0.70


# 2. 用字典做策略注册表
DISCOUNT_STRATEGIES: Dict[str, Callable[[Order], float]] = {
    "normal": normal_discount,
    "vip": vip_discount,
    "svip": svip_discount,
}

DEFAULT_STRATEGY = normal_discount


# 3. 上下文:只负责查表和调用,不关心计算细节
def calc_discount(order: Order, user_level: str) -> float:
    strategy = DISCOUNT_STRATEGIES.get(user_level, DEFAULT_STRATEGY)
    return strategy(order)


# 使用
order = Order(amount=299.0)
print(calc_discount(order, "vip"))      # 254.15
print(calc_discount(order, "unknown"))  # 284.05,走默认策略

新增一个渠道折扣,只需要加一个函数并注册一行,calc_discount 一个字都不用改:

python 复制代码
def channel_discount(order: Order) -> float:
    return order.amount * (0.9 if order.channel == "miniapp" else 0.95)

DISCOUNT_STRATEGIES["channel"] = channel_discount

如果策略本身有状态(比如要查数据库、要读配置),换成类更合适:

python 复制代码
from abc import ABC, abstractmethod


class DiscountStrategy(ABC):
    @abstractmethod
    def apply(self, order: Order) -> float: ...


class CouponStrategy(DiscountStrategy):
    def __init__(self, coupon_service):
        self.coupon_service = coupon_service   # 依赖注入进来

    def apply(self, order: Order) -> float:
        coupon = self.coupon_service.get_best(order)
        return order.amount - coupon.value
四、Java 版本:标准写法

Java 这边是教科书形态,但同样不复杂:

java 复制代码
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// 1. 策略接口
public interface DiscountStrategy {
    BigDecimal apply(Order order);
}

// 2. 具体策略
public class VipDiscountStrategy implements DiscountStrategy {
    @Override
    public BigDecimal apply(Order order) {
        return order.getAmount().multiply(new BigDecimal("0.85"));
    }
}

public class SvipDiscountStrategy implements DiscountStrategy {
    @Override
    public BigDecimal apply(Order order) {
        return order.getAmount().multiply(new BigDecimal("0.70"));
    }
}

// 3. 上下文 + 策略注册表(Spring 环境下可以更优雅,见下文)
public class DiscountCalculator {

    private static final Map<String, DiscountStrategy> REGISTRY = new ConcurrentHashMap<>();
    private static final DiscountStrategy DEFAULT = Order::getAmount;

    static {
        REGISTRY.put("vip", new VipDiscountStrategy());
        REGISTRY.put("svip", new SvipDiscountStrategy());
    }

    public static BigDecimal calc(Order order, String level) {
        return REGISTRY.getOrDefault(level, DEFAULT).apply(order);
    }
}

如果项目用了 Spring,连 static 注册表都可以省掉,靠容器自动装配:

java 复制代码
@Service
public class DiscountCalculator {

    private final Map<String, DiscountStrategy> registry;

    // 关键:Spring 会把所有 DiscountStrategy 的实现类注入进这个 Map
    // key 默认是 bean 名称,正好拿来当策略名
    public DiscountCalculator(Map<String, DiscountStrategy> strategies) {
        this.registry = new ConcurrentHashMap<>(strategies);
    }

    public BigDecimal calc(Order order, String level) {
        DiscountStrategy strategy = registry.getOrDefault(level, registry.get("default"));
        return Optional.ofNullable(strategy).orElse(IdentityStrategy.INSTANCE).apply(order);
    }
}

@Component("vip")
public class VipDiscountStrategy implements DiscountStrategy { ... }

@Component("svip")
public class SvipDiscountStrategy implements DiscountStrategy { ... }

到这里你会发现:新增规则 = 新增一个类 + 一个注解,原有代码零修改。这就是开闭原则最直观的体现。

五、几个容易踩的坑

1. 别为了模式而模式。 如果分支只有两个,且半年不会变,if-else 完全没问题。抽象是有成本的,多出来的十几个文件会让新人找入口找半天。我的经验法则是:同一个判断维度超过 4 个分支,或者预计会持续增长,才值得抽策略。

2. 策略之间不要互相依赖。 A 策略里调 B 策略,链路会迅速变成网状。如果确实需要组合,请明确引入"组合策略"(Composite),让它成为显式的一层,而不是在策略内部偷偷耦合。

3. 别忘了兜底。 注册表查不到时直接抛异常是最常见的线上故障来源之一。一定要给默认策略,或者至少记录一条清晰的 warn 日志,把入参和命中的 key 打出来。

4. 策略名不要用魔法字符串裸传。 Python 可以用 Enum,Java 直接用枚举做 key,配合编译期检查,能避免大量手滑导致的 bug。

六、什么时候该上更重的方案

策略模式解决的是"分支多"的问题。但如果你的场景是:

  • 规则需要运营在后台动态配置、随时上下架;
  • 规则之间存在优先级、互斥、叠加等组合关系;
  • 规则数量上百,且变更频率以天计;

那么就该考虑规则引擎(如 Drools、LiteFlow、Aviator)或者把规则外置到配置中心/数据库里了。策略模式是重构的第一步,不是终点。

小结
改造前 改造后
新增规则 修改主函数,回归测试全量 新增一个类/函数,单测覆盖自己
可读性 深层嵌套,要看完整段才知道做什么 类名即文档
可测性 构造各种分支组合的入参 每个策略独立测试
风险 改一处影响全部 影响面隔离

设计模式的价值从来不是让代码看起来高级,而是让改动变得安全。策略模式恰好是投入产出比最高的那一个:半小时就能学会,当天就能用上。

相关推荐
tachibana21 小时前
Go 语言里的 Context 是什么?
开发语言·后端·golang·go·语言
鶴哥只手遮天2 小时前
Spring Framework 7.0.9 深度剖析-05-配置元数据解析:XML、注解与 JavaConfig
java·后端·开源
鶴哥只手遮天2 小时前
Spring Framework 7.0.9 深度剖析-16-异步与调度
java·后端·开源
高频因子挖掘机2 小时前
500只股票 × 多年日线:数据获取与本地存储如何分工?
后端·github·api
张立立2 小时前
取名不想只靠翻字典?我做了个诗词取名工具,还能分析三才五格
后端
弈栈录2 小时前
大模型服务抽象:兼容 OpenAI、通义千问与本地模型
java·后端·架构
鶴哥只手遮天2 小时前
Spring Framework 7.0.9 深度剖析-06-Bean 生命周期与依赖注入原理
java·后端·开源
golang学习记3 小时前
最新版VS Code开发Spring Boot项目保姆级教程
java·spring boot·vscode·后端
高频因子挖掘机3 小时前
别急着搭数据平台:量化策略开发如何用最小闭环验证想法
后端·github·api