一天一个设计模式——开闭原则

什么是开闭原则?

开闭原则(OCP)的核心思想是:
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

  • 对扩展开放:当需要添加新功能时,可以通过添加新代码(比如新类或新方法)来扩展系统。
  • 对修改关闭:已有代码(尤其是核心代码)不应该被修改,以避免引入错误或影响现有功能。
通俗解释

想象你在经营一家披萨店,菜单上有 Margherita 披萨和 Pepperoni 披萨:

  • 如果每次有新口味(比如加个海鲜披萨),你都得改菜单的打印代码、价格计算代码、制作流程代码,那会很麻烦,还可能把原有披萨的逻辑改错。
  • 开闭原则就像设计一个"模板":菜单系统允许添加新披萨(扩展),但不需要改动已有披萨的代码(关闭修改)。比如,你可以定义一个"披萨基类",新口味只需继承它,添加自己的配料和价格逻辑。

这样,系统既灵活又稳定,新功能不会破坏旧功能。


为什么需要开闭原则?

  1. 降低维护成本:修改现有代码可能引入 bug,特别是在大型项目中。OCP 通过扩展而非修改,减少了出错风险。
  2. 提高扩展性:新需求可以通过新增代码实现,系统更容易适应变化。
  3. 增强代码复用:通过抽象和继承,核心逻辑可以被复用,新功能只需实现特定部分。
  4. 便于测试:已有功能代码不变,测试用例无需重写,只需测试新扩展的部分。

违反开闭原则的例子

假设我们要实现一个图形面积计算器,支持计算圆形和矩形的面积。如果不遵循开闭原则,代码可能是这样的:

java 复制代码
public class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.getRadius() * circle.getRadius();
        } else if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.getWidth() * rectangle.getHeight();
        }
        return 0;
    }
}

class Circle {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }
}

class Rectangle {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    public double getWidth() {
        return width;
    }

    public double getHeight() {
        return height;
    }
}

class Main {
    public static void main(String[] args) {
        AreaCalculator calculator = new AreaCalculator();
        Circle circle = new Circle(5);
        Rectangle rectangle = new Rectangle(4, 6);
        System.out.println("圆形面积: " + calculator.calculateArea(circle));
        System.out.println("矩形面积: " + calculator.calculateArea(rectangle));
    }
}

问题

  • 如果要添加新图形(比如三角形),必须修改 AreaCalculatorcalculateArea 方法,增加新的 else if 分支。
  • 每次添加新图形都要改动 AreaCalculator,违反了"对修改关闭"。
  • 修改可能引入错误,比如不小心改错了圆形或矩形的计算逻辑。
  • 代码中的 instanceof 判断会导致代码复杂,难以维护。

符合开闭原则的改进

为了遵循开闭原则,我们可以通过抽象(如接口或抽象类)来设计系统,让新图形通过扩展实现,而无需修改现有代码。

java 复制代码
// 定义一个抽象接口,所有图形都实现它
public interface Shape {
    double calculateArea();
}

// 圆形实现 Shape 接口
public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

// 矩形实现 Shape 接口
public class Rectangle implements Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double calculateArea() {
        return width * height;
    }
}

// 新增三角形,无需修改 AreaCalculator
public class Triangle implements Shape {
    private double base;
    private double height;

    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    public double calculateArea() {
        return 0.5 * base * height;
    }
}

// 面积计算器,依赖抽象接口
public class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.calculateArea();
    }
}

public class Main {
    public static void main(String[] args) {
        AreaCalculator calculator = new AreaCalculator();
        Shape circle = new Circle(5);
        Shape rectangle = new Rectangle(4, 6);
        Shape triangle = new Triangle(3, 4);

        System.out.println("圆形面积: " + calculator.calculateArea(circle));
        System.out.println("矩形面积: " + calculator.calculateArea(rectangle));
        System.out.println("三角形面积: " + calculator.calculateArea(triangle));
    }
}

改进后的好处

  1. 对扩展开放 :添加新图形(如 Triangle)只需实现 Shape 接口,无需修改 AreaCalculator
  2. 对修改关闭AreaCalculator 的代码保持不变,核心逻辑稳定。
  3. 代码清晰:每个图形类负责自己的面积计算,逻辑清晰,易于测试。
  4. 复用性强Shape 接口可以被其他功能复用,比如计算周长。

如何在实际项目中应用开闭原则?

作为初学者,你可以按照以下步骤在编码时应用开闭原则:

  1. 抽象化设计

    • 识别系统中可能变化的部分(比如新增图形、新的通知方式),用接口或抽象类定义通用行为。
    • 例如,定义 Shape 接口,规定所有图形必须实现 calculateArea 方法。
  2. 依赖抽象而非具体实现

    • 让核心逻辑(如 AreaCalculator)依赖接口(如 Shape),而不是具体的类(如 Circle)。
    • 使用依赖注入(Dependency Injection)传递抽象接口的实例。
  3. 使用组合或继承

    • 通过继承接口或抽象类来扩展功能,比如新增 Triangle 类。
    • 或者使用组合模式,将功能拆分成小模块,通过组合实现扩展。
  4. 避免条件分支

    • 避免在代码中使用大量 if-elseinstanceof 判断,这些通常是违反 OCP 的信号。
    • 用多态(Polymorphism)替代条件判断。
  5. 测试驱动开发

    • 编写单元测试,确保新扩展不会破坏现有功能。
    • 例如,测试 AreaCalculatorCircleRectangle 的计算正确,再添加 Triangle 测试。

实际案例:结合项目

我们可以找到开闭原则的应用场景:

前端(Vue 示例)

formStorageQuantity.vue 中,loadChartData 方法专门负责加载柱状图数据,loadBizInventoryEquipmentWidgetData 方法负责加载表格数据。如果需要添加新的图表类型(比如饼图),可以这样做:

  • 定义一个抽象的 ChartDataLoader 接口,包含 loadData 方法。
  • 当前的 loadChartData 实现柱状图逻辑,新的饼图可以新增一个类实现 ChartDataLoader
  • 修改 formStorageQuantity.vuerefreshFormStorageQuantity 方法,让它根据配置选择不同的 ChartDataLoader
javascript 复制代码
// 抽象接口(模拟)
class ChartDataLoader {
  loadData() {
    throw new Error("必须实现 loadData 方法");
  }
}

// 柱状图数据加载器
class BarChartDataLoader extends ChartDataLoader {
  loadData(params) {
    // 当前的 loadChartData 逻辑
    console.log("加载柱状图数据", params);
    // 假设调用 API 返回数据
  }
}

// 饼图数据加载器(扩展)
class PieChartDataLoader extends ChartDataLoader {
  loadData(params) {
    console.log("加载饼图数据", params);
    // 新增的饼图逻辑
  }
}

// 在 Vue 组件中使用
export default {
  data() {
    return {
      chartDataLoader: new BarChartDataLoader(), // 可切换为 PieChartDataLoader
    };
  },
  methods: {
    loadChartData() {
      this.chartDataLoader.loadData({ /* 参数 */ });
    },
  },
};

效果 :新增饼图只需实现 PieChartDataLoader,无需修改 loadChartData 的核心逻辑,符合 OCP。

后端(Java 示例)

BizInventoryEquipmentController.listWithGroup 中,当前代码支持按任意字段分组(通过 groupParam)。如果需要支持新的聚合方式(比如按 equipment_type 统计),可以:

  • 定义一个 GroupStrategy 接口,包含 buildGroupQuery 方法。
  • 当前的 listWithGroup 使用默认的 GroupByFieldStrategy
  • 新增聚合方式时,创建新的策略类(如 GroupByEquipmentTypeStrategy),无需修改控制器。
java 复制代码
public interface GroupStrategy {
    String buildGroupQuery(MyGroupParam groupParam);
}

public class GroupByFieldStrategy implements GroupStrategy {
    @Override
    public String buildGroupQuery(MyGroupParam groupParam) {
        return MyGroupParam.buildGroupBy(groupParam, BizInventoryEquipment.class);
    }
}

public class BizInventoryEquipmentController {
    @Autowired
    private GroupStrategy groupStrategy; // 注入策略

    @PostMapping("/listWithGroup")
    public ResponseResult<MyPageData<BizInventoryEquipmentVo>> listWithGroup(
            @MyRequestBody BizInventoryEquipmentDto bizInventoryEquipmentDtoFilter,
            @MyRequestBody(required = true) MyGroupParam groupParam,
            @MyRequestBody MyOrderParam orderParam,
            @MyRequestBody MyPageParam pageParam) {
        String orderBy = MyOrderParam.buildOrderBy(orderParam, BizInventoryEquipment.class, false);
        String groupBy = groupStrategy.buildGroupQuery(groupParam); // 使用策略
        if (groupBy == null) {
            return ResponseResult.error(
                    ErrorCodeEnum.INVALID_ARGUMENT_FORMAT, "数据参数错误,分组参数不能为空!");
        }
        // 其余逻辑不变
    }
}

效果 :新增分组方式只需实现新的 GroupStrategy,无需修改 listWithGroup,符合 OCP。


注意事项

  1. 抽象的粒度

    • 不要过度抽象,比如为每个小功能都定义接口,可能导致代码复杂。
    • 抽象应针对可能变化的部分,比如图形类型、通知方式等。
  2. 平衡复杂度

    • 开闭原则会增加接口或类的数量,初学者可能觉得复杂。开始时可以从简单场景入手,比如用接口分离变化点。
  3. 与单一职责原则结合

    • OCP 常与单一职责原则(SRP)一起使用。确保每个类的职责单一,再通过抽象扩展功能。
  4. 常见实现方式

    • 接口/抽象类 :如上例中的 Shape 接口。
    • 策略模式:定义行为接口,动态切换实现。
    • 工厂模式:通过工厂创建扩展对象。
    • 装饰者模式:在不修改原有类的情况下,动态添加功能。

练习:尝试应用开闭原则

假设你在开发一个支付系统,支持微信支付和支付宝支付,代码如下:

java 复制代码
public class PaymentProcessor {
    public void processPayment(String paymentType, double amount) {
        if (paymentType.equals("WeChat")) {
            System.out.println("处理微信支付: " + amount);
        } else if (paymentType.equals("Alipay")) {
            System.out.println("处理支付宝支付: " + amount);
        }
    }
}

练习任务

  1. 重构代码,使其符合开闭原则,支持新增银联支付(UnionPay)而无需修改 PaymentProcessor
  2. 写一个 main 方法,测试微信支付、支付宝支付和银联支付。
  3. 思考:如果新增 PayPal 支付,需要改哪些代码?

参考答案

java 复制代码
public interface Payment {
    void processPayment(double amount);
}

public class WeChatPayment implements Payment {
    @Override
    public void processPayment(double amount) {
        System.out.println("处理微信支付: " + amount);
    }
}

public class AlipayPayment implements Payment {
    @Override
    public void processPayment(double amount) {
        System.out.println("处理支付宝支付: " + amount);
    }
}

public class UnionPayPayment implements Payment {
    @Override
    public void processPayment(double amount) {
        System.out.println("处理银联支付: " + amount);
    }
}

public class PaymentProcessor {
    public void processPayment(Payment payment, double amount) {
        payment.processPayment(amount);
    }
}

public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new PaymentProcessor();
        Payment weChat = new WeChatPayment();
        Payment alipay = new AlipayPayment();
        Payment unionPay = new UnionPayPayment();

        processor.processPayment(weChat, 100);
        processor.processPayment(alipay, 200);
        processor.processPayment(unionPay, 300);
    }
}

练习答案分析

  • 新增 PayPal 支付只需创建 PayPalPayment 类实现 Payment 接口,无需修改 PaymentProcessor
  • 符合 OCP:对扩展开放(新增支付方式),对修改关闭(核心逻辑不变)。

总结

  • 开闭原则:对扩展开放,对修改关闭,通过抽象和多态实现灵活扩展。
  • 好处:降低维护成本、提高扩展性、增强复用性和测试性。
  • 实现方法:使用接口或抽象类,依赖抽象,结合策略模式、工厂模式等。
  • 初学者建议:从简单场景入手,识别变化点,定义接口隔离变化,逐步体会 OCP 的优势。
相关推荐
李广坤4 小时前
模板方法模式(Template Method Pattern)
设计模式
jerryinwuhan5 小时前
LINUX复习资料(二)
linux·运维·服务器
郝学胜-神的一滴5 小时前
Linux下的阻塞与非阻塞模式详解
linux·服务器·开发语言·c++·程序人生·软件工程
一枚正在学习的小白6 小时前
PG数据文件位置迁移
linux·运维·服务器·数据库
我科绝伦(Huanhuan Zhou)8 小时前
Systemctl 与 Systemd 全面指南:Linux 系统服务管理详解
linux·服务器·网络
gplitems1238 小时前
Petslist – Pet listing WordPress Theme Free Download
linux·服务器·前端
1白天的黑夜18 小时前
Linux (5)| 入门进阶:Linux 权限管理的基础规则与实践
linux·运维·服务器·centos
济南java开发,求内推8 小时前
mongodb一个服务器部署多个节点
服务器·数据库·mongodb
world-wide-wait10 小时前
python高级04——网络编程
linux·服务器·网络