单一职责原则(SRP)核心回顾
一个类 / 函数 / 模块应该仅有一个引起它变化的原因。换句话说,一个实体只承担一个 "独立且明确" 的职责,判断是否违反的关键不是 "功能多少",而是 "修改的触发因素"------ 如果两个功能的修改原因完全不同,就应该拆分。
举个通俗的类比:如果把 "洗衣机" 设计成同时具备 "洗衣""做饭""扫地" 的功能,那么 "洗衣程序升级" 可能会影响 "做饭功能","扫地逻辑修改" 也可能导致洗衣机无法正常洗衣,这就是典型的违反 SRP,维护和修改的风险会呈指数级上升。
文章目录
-
- 单一职责原则(SRP)核心回顾
- [1. 违反 SRP 的代码实现](#1. 违反 SRP 的代码实现)
- [2. 违反 SRP 带来的具体危害(结合代码分析)](#2. 违反 SRP 带来的具体危害(结合代码分析))
- [3. 符合单一职责原则的重构示例](#3. 符合单一职责原则的重构示例)
- 总结
1. 违反 SRP 的代码实现
这个UserDataHandler类同时承担了 3 个完全独立的职责:
- 职责 1:读取本地文件中的用户数据(文件 IO);
- 职责 2:验证用户数据的合法性(数据校验);
- 职责3:将校验结果写入日志文件(日志记录)。
cpp
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
using namespace std;
// 违反单一职责原则:一个类承担3个独立职责
class UserDataHandler {
private:
string logFilePath = "data_log.txt"; // 日志文件路径
// 职责3:写入日志(私有辅助函数)
void writeLog(const string& msg) {
ofstream logFile(logFilePath, ios::app);
logFile << "[" << __TIME__ << "] " << msg << endl;
logFile.close();
}
public:
// 职责1:读取文件中的用户数据
vector<string> readUserDataFromFile(const string& filePath) {
vector<string> userData;
ifstream file(filePath);
if (!file.is_open()) {
writeLog("文件打开失败:" + filePath); // 直接调用日志功能
return userData;
}
string line;
while (getline(file, line)) {
userData.push_back(line);
}
file.close();
writeLog("成功读取" + to_string(userData.size()) + "条用户数据");
return userData;
}
// 职责2:验证用户数据合法性(比如手机号格式)
bool validateUserData(const string& userData) {
// 简单校验:假设数据是"姓名,手机号",手机号必须11位数字
size_t commaPos = userData.find(',');
if (commaPos == string::npos) {
writeLog("数据格式错误:无分隔符 | " + userData);
return false;
}
string phone = userData.substr(commaPos + 1);
if (phone.length() != 11) {
writeLog("手机号非法:" + phone + " | 数据:" + userData);
return false;
}
writeLog("数据校验通过:" + userData);
return true;
}
};
// 测试代码
int main() {
UserDataHandler handler;
// 读取文件数据
vector<string> dataList = handler.readUserDataFromFile("users.txt");
// 校验每条数据
for (const string& data : dataList) {
handler.validateUserData(data);
}
return 0;
}
2. 违反 SRP 带来的具体危害(结合代码分析)
这个类看似 "功能齐全",但在实际开发中会暴露出一系列致命问题,每一个问题都直接源于 "多职责耦合":
危害 1:修改一处逻辑,引发其他功能异常(牵一发而动全身)
比如你想优化日志格式(比如加日期而非仅时间),修改writeLog函数时:
cpp
// 修改日志格式:加日期
void writeLog(const string& msg) {
ofstream logFile(logFilePath, ios::app);
// 不小心把 __TIME__ 写成 __DATE__(手误)
logFile << "[" << __DATE__ << "] " << msg << endl;
logFile.close();
}
这个看似只改 "日志职责" 的小错误,会直接导致整个UserDataHandler类的所有功能(文件读取、数据校验)的日志输出异常 ------ 甚至如果修改时误删了logFile.close(),还会导致文件句柄泄漏,进而让 "读取用户数据" 的功能因为文件资源占用而失败。
危害 2:维护成本高,需求变更时需反复修改同一个类
如果产品要求 "数据校验规则升级(比如手机号加区号)",你需要改validateUserData,同时还要改里面的日志输出逻辑;
如果要求 "读取数据从本地文件改成网络接口",你需要改readUserDataFromFile,还要确保修改后不影响日志和校验功能;
每次需求变更,这个类都要被修改,成为整个项目的 "修改重灾区",出错概率大幅提升。
危害 3:代码复用性极差
如果另一个模块只想 "复用数据校验功能"(比如校验前端传过来的用户数据),你不得不把整个UserDataHandler类引入 ------ 但这个类还包含了文件读取和日志的无关代码,不仅增加了模块体积,还可能因为日志文件路径、文件 IO 的依赖导致新模块出现意外问题(比如日志文件权限不足)。
危害 4:测试困难,无法单独测试某一个职责
想单元测试 "数据校验功能" 时,必须先处理文件读取和日志的依赖:
- 即使只是测试 "手机号长度校验",也会触发日志写入(生成不必要的日志文件);
- 如果测试环境没有日志文件的写入权限,即使校验逻辑正确,测试也会失败;
- 无法隔离测试单一职责,测试效率和准确性都极低。
3. 符合单一职责原则的重构示例
将 3 个职责拆分为 3 个独立的类,每个类只负责一件事,职责边界清晰:
cpp
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
using namespace std;
// 职责1:仅处理文件读取(文件IO模块)
class FileDataReader {
public:
vector<string> readLines(const string& filePath) {
vector<string> lines;
ifstream file(filePath);
if (!file.is_open()) {
return lines;
}
string line;
while (getline(file, line)) {
lines.push_back(line);
}
file.close();
return lines;
}
};
// 职责2:仅处理数据校验(数据校验模块)
class UserDataValidator {
public:
bool validate(const string& userData) {
size_t commaPos = userData.find(',');
if (commaPos == string::npos) {
return false;
}
string phone = userData.substr(commaPos + 1);
return phone.length() == 11;
}
};
// 职责3:仅处理日志记录(日志模块)
class LogWriter {
private:
string logFilePath = "data_log.txt";
public:
void write(const string& msg) {
ofstream logFile(logFilePath, ios::app);
logFile << "[" << __TIME__ << "] " << msg << endl;
logFile.close();
}
};
// 高层逻辑:组装各个单一职责的类(依赖组合而非耦合)
int main() {
FileDataReader reader;
UserDataValidator validator;
LogWriter logger;
// 读取数据
vector<string> dataList = reader.readLines("users.txt");
logger.write("成功读取" + to_string(dataList.size()) + "条用户数据");
// 校验数据
for (const string& data : dataList) {
bool isValid = validator.validate(data);
if (isValid) {
logger.write("数据校验通过:" + data);
} else {
logger.write("数据校验失败:" + data);
}
}
return 0;
}
重构后的优势:
- 修改日志格式?只改LogWriter,不影响读取和校验;
- 升级校验规则?只改UserDataValidator,其他类完全不动;
- 想复用校验功能?直接引入UserDataValidator即可,无无关依赖;
- 测试校验功能?只需实例化UserDataValidator,无需处理文件和日志。
总结
单一职责原则的核心是按 "变化原因" 拆分职责,而非简单按 "功能数量" 拆分;
违反 SRP 的核心危害:代码耦合度高、修改易引发连锁错误、维护成本高、复用性和可测试性差;
遵循 SRP 的关键:让每个类只承担一个 "独立职责",通过 "类的组合" 实现功能复用,而非把所有功能堆砌在一个类中。
简单来说,遵守单一职责原则的本质是 "让代码各司其职",每个类只关注自己的核心工作,从而大幅降低代码的维护成本和出错风险。