单一职责原则(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 类里。结果每次改支付逻辑,都要重新测试订单处理——明明没改那部分代码。后来拆成 OrderProcessor、PaymentValidator、AuditLogger 三个类,世界清净了。
1.4 知识体系:SRP 的核心逻辑
下面这张图,是我自己总结的 SRP 知识结构。你可以看到,职责的定义、粒度控制、以及如何判断是否违反 SRP,是三个核心维度。
1.5 违反 SRP 的典型症状
在实际项目中,怎么快速判断一个类是否违反了 SRP?我总结了几个“坏味道”:
- 类名太模糊:比如
Manager、Utils、Helper——这些名字本身就说明这个类什么都干 - public 方法太多:如果一个类有十几个 public 方法,而且它们做的事情五花八门,那大概率是职责过重
- 修改一个功能,影响另一个功能:这是最直接的信号。如果你改了 A 功能,结果 B 功能的测试挂了,说明它们不该在一起
- 类依赖太多外部资源:比如一个类同时依赖数据库、文件系统、网络、消息队列——它承担了太多职责
注意:不要为了遵守 SRP 而过度拆分。我曾经见过一个项目,把 User 类拆成了 UserName、UserEmail、UserPassword……这完全是走火入魔。职责拆分要基于“变化原因”,而不是基于“数据字段”。
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