设计原则之单一职责原则

单一职责原则(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 的关键:让每个类只承担一个 "独立职责",通过 "类的组合" 实现功能复用,而非把所有功能堆砌在一个类中。

简单来说,遵守单一职责原则的本质是 "让代码各司其职",每个类只关注自己的核心工作,从而大幅降低代码的维护成本和出错风险。

相关推荐
linzihahaha1 天前
C++ 单例模式总结
开发语言·c++·单例模式
一个不知名程序员www1 天前
算法学习入门--- set与map(C++)
c++·算法
CS创新实验室1 天前
正态分布的深入学习:从数学发现到自然法则的演变
学习·数据挖掘·数据分析·统计学·正态分布
半夏知半秋1 天前
rust学习-Option与Result
开发语言·笔记·后端·学习·rust
王夏奇1 天前
Python库学习-标准库
学习
小此方1 天前
Re: 从零开始的C++ 入門(十)类和对象·最终篇下:类型转换、static成员、友元、匿名对象、内部类、拷贝编译优化
开发语言·c++·底层
王老师青少年编程1 天前
2025年12月GESP(C++)考级真题及详细题解(汇总版)
c++·题解·真题·gesp·csp·信奥赛·考级
wenxin-1 天前
NS3学习-Packet数据包结构
网络·学习·ns3·ns3内核
_OP_CHEN1 天前
【算法基础篇】(四十二)数论之欧拉函数深度精讲:从互质到数论应用
c++·算法·蓝桥杯·数论·欧拉函数·算法竞赛·acm/icpc