软件设计原则之开闭原则

开闭原则:让你的代码更优雅地面对变化

在软件开发中,我们经常面对需求的不断变更与功能的持续扩展。如果每次变更都需要改动大量旧代码,不仅效率低,还容易引入新的 bug。这时,设计原则中的“开闭原则”(Open-Closed Principle, OCP)就显得尤为重要。

一、什么是开闭原则

定义:

软件实体(类、模块、方法等)对扩展开放,对修改关闭。

简单来说:

  • 对扩展开放(Open for extension):当需求变化时,系统可以通过添加新代码来适应,而不是改动原来的代码。

  • 对修改关闭(Closed for modification):一旦系统模块稳定,就不应该轻易修改其内部逻辑,以免破坏已有功能。

这种设计思想能显著提高系统的可维护性、可复用性和可扩展性。

二、为什么要遵循开闭原则?

在实际开发中,项目上线后往往还会持续迭代,需求也会不断变动。如果每次都修改原来的代码:

  • 很容易破坏已有功能;

  • 引入新的 bug;

  • 影响已有用户;

  • 难以维护和测试。

开闭原则的核心价值在于——将“变化”隔离,把“稳定”沉淀。也就是说,稳定的代码不动,把所有变化都通过“扩展”来实现。

✦ 如何实现变化的隔离?

我们通过抽象接口 + 多态实现,将可变逻辑(变化点)封装到独立的策略类、扩展类中,而主流程(稳定部分)则依赖这些接口。

这样,主流程永远不需要因为新增的规则而修改自己,只要新增一个扩展类,实现接口,并注册进去即可。

这就像造房子时,提前预留了插座和接口,后期你可以插不同的电器,但不需要敲墙重新布线。

三、一个真实的生产场景

我们以“订单价格计算系统”为例。公司要求对不同的用户类型使用不同的价格策略:

普通用户:原价

VIP 用户:9 折

员工:7 折

初始版本(违背开闭原则):

点击查看代码
public class PriceCalculator {

    /**
     * 计算价格
     * @param basePrice 基础价格
     * @param userType 用户类型
     * @return 价格
     */
    public BigDecimal calculatePrice(BigDecimal basePrice, String userType) {

        if ("NORMAL".equals(userType)) {
            return basePrice;
        } else if ("VIP".equals(userType)) {
            return basePrice.multiply(new BigDecimal("0.9"));
        } else if ("EMPLOYEE".equals(userType)) {
            return basePrice.multiply(new BigDecimal("0.7"));
        }
        return basePrice;
    }
}

这种方式的问题是:

  • 每加一个用户类型都要改 PriceCalculator;

  • if-else 很快就变得难以维护;

不符合开闭原则。

四、重构:使用策略模式 + 开闭原则

  1. 定义用户类型枚举
点击查看代码
@AllArgsConstructor
@Getter
public enum UserTypeEnum {

    NORMAL("NORMAL", "普通用户"),
    VIP("VIP", "VIP用户"),;

    private final String code;
    private final String desc;
}
2. 定义价格策略接口:
点击查看代码
public interface PriceStrategy {
    UserTypeEnum userType();
    BigDecimal calculate(BigDecimal basePrice);
}
  1. 实现不同的策略类:
点击查看代码
@Component
public class NormalUserStrategy implements PriceStrategy {
    @Override
    public UserTypeEnum userType() {
        return UserTypeEnum.NORMAL;
    }

    @Override
    public BigDecimal calculate(BigDecimal basePrice) {
        return basePrice;
    }
}

@Component
public class VipUserStrategy implements PriceStrategy {
    @Override
    public UserTypeEnum userType() {
        return UserTypeEnum.VIP;
    }

    @Override
    public BigDecimal calculate(BigDecimal basePrice) {
        return basePrice.multiply(new BigDecimal("0.9"));
    }
}

  1. 策略上下文(策略选择器):
点击查看代码
/**
 * @author yumai
 * @version JDK 8
 * @date 2025/5/27
 * @description  策略上下文
 */
@Component
public class PriceStrategyContext implements InitializingBean {

    /**注入策略实现列表*/
    @Autowired
    private List<PriceStrategy> priceStrategies;

    private static Map<UserTypeEnum, PriceStrategy> strategyMap = new HashMap<>();

    /**
     * 初始化策略实现在map<userTypeEnum, priceStrategy>,防止重复,便于后续根据用户类型获取策略实现
     *
     * @throws Exception 异常
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        priceStrategies.forEach(strategy -> {
            PriceStrategy priceStrategy = strategyMap.get(strategy.userType());
            if (Objects.nonNull(priceStrategy)){
                throw new RuntimeException("存在相同的策略类型:" + strategy.userType());
            }
            strategyMap.put(strategy.userType(), strategy);
        });
    }

    /**
     * 根据用户类型计算价格 (根据具体的用户类型,选择对应的策略实现,并调用其calculate方法)
     * @param userType 类型
     * @param basePrice 基础价格
     * @return 价格
     */
    public BigDecimal calculate(UserTypeEnum userType, BigDecimal basePrice) {
        PriceStrategy priceStrategy = strategyMap.get(userType);
        if (Objects.isNull(priceStrategy)){
            throw new RuntimeException("不存在策略类型:" + userType);
        }
        return priceStrategy.calculate(basePrice);
    }
}
  1. 客户端使用
点击查看代码
@RestController
@RequestMapping(value = "/price")
public class PriceController {

    @Autowired
    private PriceStrategyContext context;

    @PostMapping
    public BigDecimal getPrice(){
        return context.calculate(UserTypeEnum.VIP, new BigDecimal("100"));
    }
}

五、扩展新功能时的好处

假设新增一个“员工用户”,享受7折,只需新建一个策略类,在枚举中增加员工用户类型:

点击查看代码
@AllArgsConstructor
@Getter
public enum UserTypeEnum {

    NORMAL("NORMAL", "普通用户"),
    VIP("VIP", "VIP用户"),
    EMPLOYEE("EMPLOYEE", "员工");
    
    private final String code;
    private final String desc;
}

@Component
public class EmployeeUserStrategy implements PriceStrategy {
    @Override
    public UserTypeEnum userType() {
        return UserTypeEnum.EMPLOYEE;
    }

    @Override
    public BigDecimal calculate(BigDecimal basePrice) {
        return basePrice.multiply(new BigDecimal("0.7"));
    }
}
不需要改动任何已有的类,完美符合开闭原则。

针对于业务上用户类型的增加,需要不同的打折逻辑,这些就是变化点,也就是我们需要开放的部分,而策略上下文、策略接口主流程的部分是稳定的。

六、结束语

开闭原则并不追求“永不修改”的代码,而是希望我们将变化封装起来,让系统在面对不断增长的需求时依然保持“稳定”。如果你希望写出易扩展、少出错、能复用的代码,开闭原则一定是你必须掌握和践行的重要准则之一。

记住:不是所有的类都需要一开始就设计得非常通用,但一旦“变化”成为常态,就必须让系统遵循开闭原则。

posted @ 2025-05-27 22:15  zongshichuan  阅读(4)  评论(0)    收藏  举报