设计原则之开闭原则

开闭原则的核心理解

开闭原则是 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 的问题)

  1. 新增运算不碰旧代码:要加平方 /
    开方,只需新增SquareOperation/SqrtOperation子类,Calculator和加减乘除子类完全不动,从根源避免破坏旧功能;
  2. 代码职责清晰:每个运算子类只负责自己的逻辑(加法只管加,除法只管除 + 除数校验),不会出现 "一锅乱炖";
    测试成本低:新增平方运算时,只需测试SquareOperation,无需重新测试加减乘除;
  3. 参数适配灵活:如果平方只需 1 个数,可在SquareOperation的calculate里忽略 b,或新增重载,不影响其他运算;
  4. 扩展性极强:哪怕要加 "三角函数运算""对数运算",只需新增对应的子类,计算器核心逻辑永远不用改。

总结

开闭原则的核心是:新增功能靠 "扩展子类" 实现(对扩展开放),绝不修改已有稳定代码(对修改关闭);
违反 OCP 的危害:修改旧代码易破坏原有功能、代码臃肿难维护、测试成本高、参数 / 逻辑耦合混乱;

C++ 落地 OCP 的关键:用抽象类(纯虚函数)定义核心行为,让高层模块(计算器)依赖抽象,具体功能(加减乘除)通过子类扩展。

简单来说,用计算器理解 OCP 就是:给计算器加新运算,只需要 "插上新的运算模块"(新增子类),而不是 "拆开机身改原有电路"(修改旧代码)。

相关推荐
奔波霸的伶俐虫24 分钟前
windows docker desktop 安装修改镜像学习
学习·docker·容器
散峰而望24 分钟前
【算法竞赛】顺序表和vector
c语言·开发语言·数据结构·c++·人工智能·算法·github
cpp_250137 分钟前
B3927 [GESP202312 四级] 小杨的字典
数据结构·c++·算法·题解·洛谷
Cx330❀40 分钟前
《C++ 递归、搜索与回溯》第2-3题:合并两个有序链表,反转链表
开发语言·数据结构·c++·算法·链表·面试
帅次1 小时前
系统设计方法论全解:原则、模型与用户体验核心要义
设计模式·流程图·软件工程·软件构建·需求分析·设计规范·规格说明书
BullSmall1 小时前
《逍遥游》
学习
小六子成长记1 小时前
【C++】:多态的实现
开发语言·c++
奔波霸的伶俐虫1 小时前
spring boot集成kafka学习
spring boot·学习·kafka
CCPC不拿奖不改名1 小时前
面向对象编程:继承与多态+面试习题
开发语言·数据结构·python·学习·面试·职场和发展
chen_2271 小时前
动态桌面方案
c++·qt·ffmpeg·kanzi