单一职责原则(SRP)上:定义与起源,一个类只负责一个职责,职责的定义与粒度控制

各位同学,今天我们来聊聊面向对象设计里最基础、也最容易被忽视的一条原则——单一职责原则。英文叫 Single Responsibility Principle,简称 SRP。

说实话,我刚开始写 C++ 的那几年,根本没把这条原则当回事。那时候觉得,一个类能多塞点功能,显得自己很厉害。直到有一次,我维护一个两千多行的 DataProcessor 类,改一个 bug 引出三个新 bug……嗯,从那以后,我彻底老实了。

1.1 什么是单一职责原则?

单一职责原则的核心思想,用一句话就能说清楚:一个类,只应该有一个引起它变化的原因。换句话说,一个类只负责一件事。

这个定义最早是 Robert C. Martin(我们圈内都叫他 Uncle Bob)在 2003 年左右提出的。他当时在分析一个 modem 类的设计时发现,如果让一个类同时处理拨号、数据收发、协议解析,那么任何一个需求变化都会导致这个类被修改——这太危险了。

核心定义:一个类应当只有一个职责,并且该职责应当被完整封装在类内部。如果存在多个职责,它们应当被分离到不同的类中。

你想想看,如果一个类同时负责数据存储和网络通信,那么当数据库从 MySQL 换成 Redis 时,你要改这个类;当网络协议从 TCP 换成 UDP 时,你还要改这个类。两个完全不相关的需求变化,却要修改同一个类——这明显不合理。

1.2 职责的定义:到底什么是“一个职责”?

这里有个很实际的问题:什么才算“一个职责”?

我个人习惯这样判断:如果一个功能变化了,会导致这个类被修改,那么这个功能就是这个类的职责之一。如果两个功能变化的原因不同,它们就不应该放在同一个类里。

举个例子,假设我们有一个 ReportGenerator 类:

// 不好的设计:一个类承担了多个职责
class ReportGenerator {
public:
    void generateReport(const std::string& data);
    void saveToFile(const std::string& filename);
    void sendByEmail(const std::string& email);
};

这个类干了三件事:生成报告、保存文件、发送邮件。如果文件保存路径变了,你要改它;如果邮件服务器变了,你还要改它。两个完全不相关的变化,却要修改同一个类——这就是典型的职责耦合。

更好的做法是拆成三个类:

// 好的设计:每个类只负责一个职责
class ReportGenerator {
public:
    void generateReport(const std::string& data);
};

class FileSaver {
public:
    void save(const std::string& content, const std::string& filename);
};

class EmailSender {
public:
    void send(const std::string& to, const std::string& content);
};

这样,每个类只有一个变化的原因。文件保存逻辑变了,只改 FileSaver;邮件发送逻辑变了,只改 EmailSender。互不干扰。

1.3 职责的粒度控制:拆多细才算合适?

这里有个常见的困惑:是不是类拆得越细越好?

当然不是。我见过一些项目,把职责拆得特别碎,一个简单的业务逻辑要跨五六个类才能跑通。代码是“灵活”了,但读起来像在玩拼图游戏。

我个人建议,粒度控制可以参考这几个原则:

  • 高内聚:一个类内部的成员应该高度相关,共同完成一个目标
  • 低耦合:类与类之间的依赖尽量少,尽量弱
  • 变化隔离:不同变化频率、不同变化原因的功能,一定要分开

我的经验:如果你发现一个类有超过三个 public 方法,而且这些方法做的事情差异很大,那就该考虑拆分了。反过来,如果两个类总是同时被修改,那它们可能应该合并。

我曾经在一个支付系统中,把订单处理、支付校验、日志记录全部塞进一个 OrderService 类里。结果每次改支付逻辑,都要重新测试订单处理——明明没改那部分代码。后来拆成 OrderProcessorPaymentValidatorAuditLogger 三个类,世界清净了。

1.4 知识体系:SRP 的核心逻辑

下面这张图,是我自己总结的 SRP 知识结构。你可以看到,职责的定义、粒度控制、以及如何判断是否违反 SRP,是三个核心维度。

单一职责原则 (SRP) 职责的定义 一个变化原因 = 一个职责 功能变化导致类修改 → 职责耦合 不同变化原因 → 应当分离 粒度控制 高内聚:类内部高度相关 低耦合:类间依赖尽量弱 变化隔离:不同频率分开 判断是否违反 类名是否模糊?(如 Utils) public 方法是否过多? 修改是否影响不相关功能? 核心:一个类,一个变化原因 拆得合理是艺术,拆得过度是灾难

1.5 违反 SRP 的典型症状

在实际项目中,怎么快速判断一个类是否违反了 SRP?我总结了几个“坏味道”:

  1. 类名太模糊:比如 ManagerUtilsHelper——这些名字本身就说明这个类什么都干
  2. public 方法太多:如果一个类有十几个 public 方法,而且它们做的事情五花八门,那大概率是职责过重
  3. 修改一个功能,影响另一个功能:这是最直接的信号。如果你改了 A 功能,结果 B 功能的测试挂了,说明它们不该在一起
  4. 类依赖太多外部资源:比如一个类同时依赖数据库、文件系统、网络、消息队列——它承担了太多职责

注意:不要为了遵守 SRP 而过度拆分。我曾经见过一个项目,把 User 类拆成了 UserNameUserEmailUserPassword……这完全是走火入魔。职责拆分要基于“变化原因”,而不是基于“数据字段”。

1.6 一个实际案例

最后,我分享一个我实际重构过的例子。这是一个 C++ 的日志系统:

// 重构前:一个类干了所有事
class Logger {
public:
    void log(const std::string& message);
    void writeToFile(const std::string& filename);
    void sendToNetwork(const std::string& host, int port);
    void formatAsJson(const std::string& message);
    void formatAsXml(const std::string& message);
};

这个 Logger 类同时负责日志记录、文件写入、网络发送、格式转换。任何一个变化都要改它。重构后:

// 重构后:每个类只负责一个职责
class LogFormatter {
public:
    std::string format(const std::string& message);
};

class FileWriter {
public:
    void write(const std::string& content, const std::string& path);
};

class NetworkSender {
public:
    void send(const std::string& content, const std::string& host, int port);
};

class Logger {
private:
    LogFormatter formatter;
    FileWriter writer;
    NetworkSender sender;
public:
    void log(const std::string& message);
};

重构后的 Logger 只负责协调,具体的格式、存储、发送都委托给专门的类。这样,任何一个职责变化,都只需要修改对应的类。

嗯,这就是单一职责原则的核心。说白了,就是让每个类专心做好一件事。你想想看,如果团队里每个人都只负责自己最擅长的事,效率是不是更高?代码也是一样的道理。


公众号:蓝海资料掘金营,微信deep3321