目录
[二、单一职责原则(Single Responsibility Principle)](#二、单一职责原则(Single Responsibility Principle))
[三、开闭原则(Open-Closed Principle)](#三、开闭原则(Open-Closed Principle))
一、为什么需要设计原则?
先看一段"能运行但很糟糕"的代码:
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;
}
};
问题:
-
单一职责:一个类做了 4 件事
-
开闭原则:新增 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 的后三个原则:子类必须能替换父类、接口要小而专、依赖抽象而非具体。这三个原则共同塑造了可扩展的系统架构。