当前位置: 首页 > news >正文

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

什么是开闭原则?

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

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

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

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

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


为什么需要开闭原则?

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

违反开闭原则的例子

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

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 判断会导致代码复杂,难以维护。

符合开闭原则的改进

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

// 定义一个抽象接口,所有图形都实现它
public interface Shape {double calculateArea();
}// 圆形实现 Shape 接口
public class Circle implements Shape {private double radius;public Circle(double radius) {this.radius = radius;}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic 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
// 抽象接口(模拟)
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),无需修改控制器。
public interface GroupStrategy {String buildGroupQuery(MyGroupParam groupParam);
}public class GroupByFieldStrategy implements GroupStrategy {@Overridepublic String buildGroupQuery(MyGroupParam groupParam) {return MyGroupParam.buildGroupBy(groupParam, BizInventoryEquipment.class);}
}public class BizInventoryEquipmentController {@Autowiredprivate 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 接口。
    • 策略模式:定义行为接口,动态切换实现。
    • 工厂模式:通过工厂创建扩展对象。
    • 装饰者模式:在不修改原有类的情况下,动态添加功能。

练习:尝试应用开闭原则

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

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 支付,需要改哪些代码?

参考答案

public interface Payment {void processPayment(double amount);
}public class WeChatPayment implements Payment {@Overridepublic void processPayment(double amount) {System.out.println("处理微信支付: " + amount);}
}public class AlipayPayment implements Payment {@Overridepublic void processPayment(double amount) {System.out.println("处理支付宝支付: " + amount);}
}public class UnionPayPayment implements Payment {@Overridepublic 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 的优势。
http://www.dtcms.com/a/465730.html

相关文章:

  • C++协程版本网络框架:快速构建一个高效极致简洁的HTTP服务器
  • 福州台江区网站建设网页怎么做链接
  • 单片机图形化编程:课程目录介绍 总纲
  • Redis-集合(Set)类型
  • 软件定义的理想硬件平台:Qotom Q30900SE/UE系列在AIO服务器与边缘网关中的实践
  • MS7126 24位立体音频DAC
  • 引领网站手机网站建设的整体流程
  • 计算机网络【第四章-网络层】
  • 响应式网站建设特征wordpress网站不显示系列
  • Fiddler抓包工具使用教程,代理设置与调试方法实战解析(含配置技巧)
  • linux系统中如何在root用户中将某个文件夹目录的权限赋值给其它用户(主要说的是 方法 1)
  • 手机网站引导页wordpress 动漫主题
  • 科技服务公司网站模版如何做视频类网站
  • 最小覆盖子串
  • 算法4.0
  • 云网智安一体:中国电信数字安全创新的技术破局与生态构建
  • 制作音乐网站实验报告佛山做外贸网站渠道
  • 企业级数据库实操手册:从架构部署到安全运维的落地指南
  • 网络安全认证培训机构的痛点
  • 网站搜索引擎推广方案做网页设计的网站
  • 国内坚持做正品的网站女人学ui有前途吗
  • centos如何做的时间同步
  • CentOS 7 环境下 RabbitMQ 的部署与 Web 管理界面基本使用指南
  • 【AT指令解析】TencentOS Tiny AT指令解析源码分析1-简介
  • centos/cuos如何开启软件源
  • Java常见业务场景之批处理优化:从稳定性、性能、数据一致性、健壮性、可观测性五大维度,系统提供批处理优化方案
  • 网站建设拟采用的技术路线深圳互联网公司招聘
  • 人工智能学习:逻辑回归
  • 23种设计模式——命令模式(Command Pattern)
  • 网站空间用万网的 域名不在万网gta5 网站正在建设中