开闭原则的核心理解
开闭原则是 SOLID 设计原则的第二个原则,核心定义可以概括为:
软件实体(类、模块、函数)应当对扩展开放(Open for extension),对修改关闭(Closed for modification)。
通俗来讲:当需要给程序新增功能时,应该通过扩展已有代码(比如新增类、新增子类)的方式实现,而不是直接修改已有、稳定的核心代码。就像你给手机加新功能,应该用外接配件(扩展),而不是拆开机身改内部电路(修改)------ 拆机改电路容易弄坏原有功能,而外接配件完全不影响手机本身的正常使用。
在 C++ 中,开闭原则的落地核心是面向抽象编程 + 多态:用抽象类(纯虚函数)定义核心行为,具体功能通过子类实现;新增功能时只加新的子类,不改动抽象类和原有子类。
文章目录
-
- 开闭原则的核心理解
- [违反开闭原则的反面例子(C++ 代码)](#违反开闭原则的反面例子(C++ 代码))
- [违反开闭原则 带来的具体危害(计算器场景)](#违反开闭原则 带来的具体危害(计算器场景))
- 符合开闭原则的计算器重构示例
- [重构后的核心优势(对比违反 OCP 的问题)](#重构后的核心优势(对比违反 OCP 的问题))
- 总结
违反开闭原则的反面例子(C++ 代码)
这个计算器类把所有运算逻辑都写死在calculate函数里,新增运算必须修改这个核心函数:
cpp
#include <iostream>
#include <stdexcept>
using namespace std;
// 违反开闭原则:所有运算逻辑耦合在一个类里,新增运算必须修改此类
class Calculator {
public:
// 核心计算函数:仅支持加减,新增乘除需直接改这里
double calculate(double a, double b, char op) {
if (op == '+') {
return a + b;
} else if (op == '-') {
return a - b;
}
// 后续加乘除,必须在这里加else if分支
else {
throw invalid_argument("不支持的运算符");
}
}
};
// 测试代码
int main() {
Calculator calc;
// 原有功能:加法
cout << "10 + 5 = " << calc.calculate(10, 5, '+') << endl;
// 原有功能:减法
cout << "10 - 5 = " << calc.calculate(10, 5, '-') << endl;
// 需求变更:新增乘法 → 必须修改Calculator的calculate函数
// 先手动改代码(加else if (op == '*')),再测试:
// cout << "10 * 5 = " << calc.calculate(10, 5, '*') << endl;
return 0;
}
违反开闭原则 带来的具体危害(计算器场景)
当你为了新增 "乘法、除法" 修改Calculator类后,会暴露以下真实开发中的问题:
危害 1:修改原有代码,极易破坏旧功能
比如你新增乘法时手误写错了加法的判断条件:
cpp
// 手误:把 '+' 写成 '='
if (op == '=') {
return a + b;
} else if (op == '-') {
return a - b;
} else if (op == '*') { // 新增乘法分支
return a * b;
}
这会导致原本正常的加法功能直接失效------ 仅仅为了加一个乘法,就搞坏了核心的加法逻辑,这是开闭原则要杜绝的核心问题。
危害 2:代码越来越臃肿,维护成本飙升
如果后续要加除法、平方、开方,你需要不断在calculate函数里加else if分支:
cpp
double calculate(double a, double b, char op) {
if (op == '+') { return a + b; }
else if (op == '-') { return a - b; }
else if (op == '*') { return a * b; }
else if (op == '/') { // 新增除法
if (b == 0) throw runtime_error("除数不能为0");
return a / b;
}
else if (op == '^') { // 新增平方(仅需a,b无用)
return a * a;
}
// 后续还要加开方、取模...
else {
throw invalid_argument("不支持的运算符");
}
}
最终这个函数会变成 "一锅乱炖":平方只需要 1 个数,但被迫传 2 个数(b 无用);除法要加除数校验,逻辑混杂在加减乘里;后期没人敢改 ------ 怕改坏任意一个分支。
危害 3:测试成本指数级增加
新增除法后,你不仅要测试 "10/2=5" 是否正确,还要重新测试加法、减法、乘法------ 因为你修改了calculate的核心逻辑,必须确认原有运算没被破坏。如果计算器有 10 种运算,新增第 11 种时要测试前 10 种,测试效率极低。
危害 4:参数适配困难,违反封装
比如新增 "平方运算"(只需要 1 个操作数),但原有calculate函数要求传 2 个参数(a 和 b),只能被迫传一个无用的 b 值(比如calc.calculate(5, 0, '^')),既不直观,又容易引发错误(比如误把 b 当成平方次数)。
符合开闭原则的计算器重构示例
核心思路:用抽象类定义运算行为,计算器依赖抽象而非具体运算;新增运算时只加新的子类,完全不修改原有代码。
cpp
#include <iostream>
#include <stdexcept>
using namespace std;
// 第一步:定义抽象运算类(对修改关闭)
// 所有运算都遵循这个抽象接口,新增运算只扩展子类,不改此类
class Operation {
public:
virtual ~Operation() = default; // 虚析构:避免子类指针析构泄漏
// 纯虚函数:定义运算的抽象行为,具体实现由子类完成
virtual double calculate(double a, double b) = 0;
};
// 第二步:实现具体运算子类(对扩展开放)
// 加法运算:仅实现加法逻辑,不影响其他运算
class AddOperation : public Operation {
public:
double calculate(double a, double b) override {
return a + b;
}
};
// 减法运算:仅实现减法逻辑
class SubOperation : public Operation {
public:
double calculate(double a, double b) override {
return a - b;
}
};
// 新增乘法运算:完全不修改原有代码,只加新类
class MulOperation : public Operation {
public:
double calculate(double a, double b) override {
return a * b;
}
};
// 新增除法运算:只加新类,原有代码零修改
class DivOperation : public Operation {
public:
double calculate(double a, double b) override {
if (b == 0) {
throw runtime_error("除数不能为0");
}
return a / b;
}
};
// 第三步:计算器类(依赖抽象,而非具体运算)
class Calculator {
public:
// 接收抽象运算类的指针,不关心具体是加减乘除
double compute(Operation* op, double a, double b) {
return op->calculate(a, b);
}
};
// 测试代码
int main() {
Calculator calc;
// 加法运算
AddOperation addOp;
cout << "10 + 5 = " << calc.compute(&addOp, 10, 5) << endl;
// 减法运算
SubOperation subOp;
cout << "10 - 5 = " << calc.compute(&subOp, 10, 5) << endl;
// 乘法运算(新增,无任何原有代码修改)
MulOperation mulOp;
cout << "10 * 5 = " << calc.compute(&mulOp, 10, 5) << endl;
// 除法运算(新增,无任何原有代码修改)
DivOperation divOp;
try {
cout << "10 / 5 = " << calc.compute(&divOp, 10, 5) << endl;
// 测试除数为0
cout << "10 / 0 = " << calc.compute(&divOp, 10, 0) << endl;
} catch (const runtime_error& e) {
cout << "错误:" << e.what() << endl;
}
return 0;
}
重构后的核心优势(对比违反 OCP 的问题)
- 新增运算不碰旧代码:要加平方 /
开方,只需新增SquareOperation/SqrtOperation子类,Calculator和加减乘除子类完全不动,从根源避免破坏旧功能; - 代码职责清晰:每个运算子类只负责自己的逻辑(加法只管加,除法只管除 + 除数校验),不会出现 "一锅乱炖";
测试成本低:新增平方运算时,只需测试SquareOperation,无需重新测试加减乘除; - 参数适配灵活:如果平方只需 1 个数,可在SquareOperation的calculate里忽略 b,或新增重载,不影响其他运算;
- 扩展性极强:哪怕要加 "三角函数运算""对数运算",只需新增对应的子类,计算器核心逻辑永远不用改。
总结
开闭原则的核心是:新增功能靠 "扩展子类" 实现(对扩展开放),绝不修改已有稳定代码(对修改关闭);
违反 OCP 的危害:修改旧代码易破坏原有功能、代码臃肿难维护、测试成本高、参数 / 逻辑耦合混乱;
C++ 落地 OCP 的关键:用抽象类(纯虚函数)定义核心行为,让高层模块(计算器)依赖抽象,具体功能(加减乘除)通过子类扩展。
简单来说,用计算器理解 OCP 就是:给计算器加新运算,只需要 "插上新的运算模块"(新增子类),而不是 "拆开机身改原有电路"(修改旧代码)。