【c++面向对象编程】第37篇:面向对象设计原则(一):单一职责与开闭原则

目录

一、为什么需要设计原则?

[二、单一职责原则(Single Responsibility Principle)](#二、单一职责原则(Single Responsibility Principle))

违反原则的例子

重构:分离职责

[三、开闭原则(Open-Closed Principle)](#三、开闭原则(Open-Closed Principle))

违反原则的例子

重构:使用多态(开闭原则的标准解法)

四、完整例子:从"万能类"到符合原则的设计

版本1:违反所有原则

版本2:重构后

五、两个原则的关系

六、常见误区

误区1:过度拆分

误区2:认为开闭原则意味着"永远不修改代码"

误区3:在错误的地方使用多态

七、这一篇的收获


一、为什么需要设计原则?

先看一段"能运行但很糟糕"的代码:

cpp

复制代码
class Employee {
public:
    void calculateSalary() { /* 计算工资 */ }
    void saveToDatabase() { /* 保存到数据库 */ }
    void generateReport() { /* 生成报表 */ }
    void sendEmail() { /* 发送邮件 */ }
};

这个类做了太多事情:工资计算、数据库操作、报表生成、邮件发送。问题来了:

  • 修改工资算法 → 改动 Employee

  • 更换数据库 → 又要改动 Employee

  • 修改邮件模板 → 还是改动 Employee

这个类成了"上帝类",每次修改都可能引入 bug,而且多个团队互相牵制。

设计原则就是为了解决这类问题


二、单一职责原则(Single Responsibility Principle)

一个类应该只有一个引起它变化的原因。

换句话说:一个类只应该做一件事。如果类有多个职责,这些职责会耦合在一起,一个职责的变化可能影响或破坏其他职责。

违反原则的例子

cpp

复制代码
// ❌ 违反单一职责:这个类既处理数据,又处理展示
class User {
private:
    string name;
    int age;
public:
    // 数据逻辑
    void setName(const string& n) { name = n; }
    string getName() const { return name; }
    
    // 展示逻辑
    void printToConsole() const {
        cout << "User: " << name << ", age: " << age << endl;
    }
    string toHtml() const {
        return "<div>" + name + "</div>";
    }
};

两个职责会在两个地方被重用:

  • printToConsole 可能只在控制台程序中使用

  • toHtml 可能只在 Web 程序中使用

如果把它们放在一起,每次修改 User 的数据结构,两边的展示代码都要重新编译。

重构:分离职责

cpp

复制代码
// ✅ 只负责数据
class User {
private:
    string name;
    int age;
public:
    void setName(const string& n) { name = n; }
    string getName() const { return name; }
    void setAge(int a) { age = a; }
    int getAge() const { return age; }
};

// ✅ 负责控制台展示
class UserConsoleRenderer {
public:
    void render(const User& user) const {
        cout << "User: " << user.getName() << ", age: " << user.getAge() << endl;
    }
};

// ✅ 负责 HTML 展示
class UserHtmlRenderer {
public:
    string render(const User& user) const {
        return "<div>" + user.getName() + "</div>";
    }
};

现在 User 类只关心数据,任何展示逻辑的变化都不会影响它。


三、开闭原则(Open-Closed Principle)

软件实体(类、模块、函数)应该对扩展开放,对修改关闭。

意思是:当需求变化时,应该添加新代码 来适应变化,而不是修改已有代码

违反原则的例子

cpp

复制代码
// ❌ 违反开闭原则:添加新形状需要修改 getArea 函数
class Rectangle {
public:
    double width, height;
};

class Circle {
public:
    double radius;
};

double getArea(void* shape, int type) {
    if (type == 1) {
        Rectangle* r = (Rectangle*)shape;
        return r->width * r->height;
    }
    else if (type == 2) {
        Circle* c = (Circle*)shape;
        return 3.14 * c->radius * c->radius;
    }
    // 添加三角形 → 必须修改这个函数!
}

每次添加新形状,getArea 都要改,风险很高。

重构:使用多态(开闭原则的标准解法)

cpp

复制代码
// ✅ 对扩展开放:新形状只需继承 Shape
class Shape {
public:
    virtual double getArea() const = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double getArea() const override { return width * height; }
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double getArea() const override { return 3.14159 * radius * radius; }
};

// 添加新形状:不需要修改任何已有代码!
class Triangle : public Shape {
    double base, height;
public:
    Triangle(double b, double h) : base(b), height(h) {}
    double getArea() const override { return 0.5 * base * height; }
};

// 这个函数永远不需要修改
double totalArea(const vector<Shape*>& shapes) {
    double sum = 0;
    for (auto s : shapes) {
        sum += s->getArea();
    }
    return sum;
}

四、完整例子:从"万能类"到符合原则的设计

假设我们有一个报表生成系统,初始需求:生成 PDF 报表。

版本1:违反所有原则

cpp

复制代码
class Report {
private:
    string title;
    vector<string> data;
public:
    Report(const string& t, const vector<string>& d) : title(t), data(d) {}
    
    // 数据库操作
    void loadFromDB() {
        cout << "从数据库加载数据..." << endl;
        // 假设加载了数据
    }
    
    // 业务逻辑
    void processData() {
        cout << "处理数据..." << endl;
        for (auto& s : data) {
            // 处理逻辑
        }
    }
    
    // PDF 生成
    void generatePDF() {
        cout << "生成 PDF 报表: " << title << endl;
        for (const auto& row : data) {
            cout << "  - " << row << endl;
        }
    }
    
    // 邮件发送
    void sendEmail(const string& recipient) {
        cout << "发送邮件到 " << recipient << endl;
    }
};

问题:

  1. 单一职责:一个类做了 4 件事

  2. 开闭原则:新增 Excel 格式 → 修改 Report

版本2:重构后

cpp

复制代码
// ========== 1. 数据类(单一职责)==========
class ReportData {
private:
    string title;
    vector<string> rows;
public:
    void setTitle(const string& t) { title = t; }
    string getTitle() const { return title; }
    void addRow(const string& row) { rows.push_back(row); }
    const vector<string>& getRows() const { return rows; }
};

// ========== 2. 数据加载接口(开闭原则)==========
class IDataLoader {
public:
    virtual ReportData load() = 0;
    virtual ~IDataLoader() = default;
};

class DatabaseLoader : public IDataLoader {
public:
    ReportData load() override {
        ReportData data;
        data.setTitle("数据库报表");
        data.addRow("行1");
        data.addRow("行2");
        cout << "从数据库加载" << endl;
        return data;
    }
};

class FileLoader : public IDataLoader {
    string filename;
public:
    FileLoader(const string& f) : filename(f) {}
    ReportData load() override {
        ReportData data;
        data.setTitle("文件报表");
        data.addRow("文件行1");
        cout << "从文件 " << filename << " 加载" << endl;
        return data;
    }
};

// ========== 3. 报表生成接口(开闭原则)==========
class IReportGenerator {
public:
    virtual void generate(const ReportData& data) = 0;
    virtual ~IReportGenerator() = default;
};

class PdfGenerator : public IReportGenerator {
public:
    void generate(const ReportData& data) override {
        cout << "生成 PDF: " << data.getTitle() << endl;
        for (const auto& row : data.getRows()) {
            cout << "  PDF: " << row << endl;
        }
    }
};

class HtmlGenerator : public IReportGenerator {
public:
    void generate(const ReportData& data) override {
        cout << "<h1>" << data.getTitle() << "</h1>" << endl;
        cout << "<ul>" << endl;
        for (const auto& row : data.getRows()) {
            cout << "  <li>" << row << "</li>" << endl;
        }
        cout << "</ul>" << endl;
    }
};

// ========== 4. 报表服务(协调者)==========
class ReportService {
    unique_ptr<IDataLoader> loader;
    unique_ptr<IReportGenerator> generator;
public:
    ReportService(unique_ptr<IDataLoader> l, unique_ptr<IReportGenerator> g)
        : loader(move(l)), generator(move(g)) {}
    
    void run() {
        ReportData data = loader->load();
        generator->generate(data);
    }
};

// ========== 使用示例 ==========
int main() {
    // 组合不同的加载器和生成器,无需修改任何已有类
    auto service1 = ReportService(
        make_unique<DatabaseLoader>(),
        make_unique<PdfGenerator>()
    );
    service1.run();
    
    auto service2 = ReportService(
        make_unique<FileLoader>("data.txt"),
        make_unique<HtmlGenerator>()
    );
    service2.run();
    
    // 如果要添加 Excel 导出:只需实现 IReportGenerator,不修改现有代码
    // class ExcelGenerator : public IReportGenerator { ... };
    
    return 0;
}

输出:

text

复制代码
从数据库加载
生成 PDF: 数据库报表
  PDF: 行1
  PDF: 行2
从文件 data.txt 加载
<h1>文件报表</h1>
<ul>
  <li>文件行1</li>
</ul>

对比

维度 版本1(违反原则) 版本2(符合原则)
单一职责 一个类做4件事 每个类只有1个职责
开闭原则 新格式需要修改原类 添加 IReportGenerator 实现即可
可测试性 难(全部耦合) 易(可独立测试每个组件)
代码复用 好(加载器可复用)

五、两个原则的关系

单一职责是开闭原则的基础。如果一个类有多个职责,那么需求的改变就会影响多个职责,违背开闭原则。

text

复制代码
单一职责 → 类只有一个变化的原因
    ↓
开闭原则 → 变化通过新增类来实现,而不是修改现有类

六、常见误区

误区1:过度拆分

cpp

复制代码
// ❌ 过度设计:一个加法器都单独成类
class Adder {
public:
    int add(int a, int b) { return a + b; }
};

原则是指导,不是教条。简单功能不需要拆分。

误区2:认为开闭原则意味着"永远不修改代码"

开闭原则不是禁止修改,而是要求对扩展开放,对修改关闭。修复 bug、重构内部实现是可以修改的,只是对外部行为保持兼容。

误区3:在错误的地方使用多态

如果形状的添加频率很低,或者所有形状都是已知的,用枚举 + switch 可能更简单。原则要结合实际情况。


七、这一篇的收获

你现在应该理解:

  • 单一职责:一个类只做一件事,只有一个引起变化的原因

  • 开闭原则:对扩展开放(可以加新类),对修改关闭(尽量不改旧类)

  • 多态是开闭原则的主要实现手段:通过抽象基类 + 派生类扩展功能

  • 好处:代码更容易维护、测试、复用

  • 警惕过度设计:原则是工具,不是目的

💡 小作业:找一个你以前写的"万能类"(比如 UserManager 既处理数据库又发送邮件),按照单一职责和开闭原则重构。至少拆成 3 个类,并用接口/多态支持扩展。


下一篇预告:第38篇《设计原则(二):里氏替换、接口隔离与依赖倒置》------继续 SOLID 的后三个原则:子类必须能替换父类、接口要小而专、依赖抽象而非具体。这三个原则共同塑造了可扩展的系统架构。

相关推荐
小明同学012 小时前
C++后端项目:统一大模型接入 SDK(三)
开发语言·c++
Brilliantwxx2 小时前
【C++】 继承与多态(下)
开发语言·c++
C+++Python2 小时前
C++考试语法知识
开发语言·c++
凯瑟琳.奥古斯特3 小时前
操作系统核心结构解析
java·开发语言·c++·python·职场和发展
郭郭的柳柳在学FPGA3 小时前
千兆以太网@——帧格式
java·开发语言·网络
handler013 小时前
【Linux 网络】一文读懂 HTTP 协议
linux·c语言·网络·c++·笔记·网络协议·http
我还记得那天3 小时前
用C语言实现一个简易扫雷小游戏
c语言·开发语言
段ヤシ.3 小时前
回顾Java知识点,面试题汇总Day10(持续更新)
java·开发语言·spring
小明同学013 小时前
C++后端项目:统一大模型接入 SDK(二)
开发语言·c++