单一职责原则
1288字约4分钟
2022-04-08
定义
单一职责原则(Single responsibility principle, SRP).
一个对象应该只包含单一的职责, 并且该职责被完整地封装在一个类中.
作用
- 一个类(大到模块, 小到方法)承担的职责越多, 它被复用的可能性就越小.
- 当一个职责变化时, 可能会影响其他职责的运作.
- 将这些职责进行分离, 将不同的职责封装在不同的类中.
- 将不同的变化原因封装在不同的类中.
- 单一职责原则是实现高内聚、低耦合的指导方针.
优点
组织代码: 让我们想象一个汽车修理工. 他使用很多工具一起工作. 这些工具按类型分为: 钳子, 螺丝刀(十字/刀片), 锤子, 扳手(管/六角)等, 他如何组织管理这些工具呢?他使用许多小抽屉将这些工具分门别类存放, 这些抽屉其实类似模块作用, 专门用来管理各种类.
减少脆弱: 当一个类有多个理由需要修改时, 它变得脆弱, 在一个地方的修改会导致其他地方不可预期的后果.
更松耦合: 更多职责责任导致更高的耦合; 耦合也是一种责任; 高度耦合导致高度依赖, 意味着难以维护.
代码改变: 对于单一职责模块重构更容易. 如果你想获得猎枪的效果, 就让你的类有更多职责.
维护性: 维护一个单一职责的类比维护一个铁板一块的类更容易.
易于测试: 测试单一目标的类只需要很少的测试类.
易于调试: 在一个单一职责类找到问题是一件更容易的事情.
如何识别 SRP 被破坏?
类有太多依赖: 类的构造器有太多参数, 意味着测试有太多依赖, 需要制造 mock 太多测试输入参数, 通常意味着已经破坏 SRP 了.
方法有太多参数: 类似类的构造器, 方法参数意味着依赖.
测试类变得复杂: 如果测试有太多变量, 意味着这个类有太多职责.
类或方法太长: 如果方法太长, 意味着内容太多, 职责过多. 一个类不超过 200-250
描述性名称: 如果你需要描述你的类 方法或包, 比如使用"xxx和xxx"这种语句, 意味着可能破坏了 SRP.
低聚合 Cohesion 的类: 聚合 Cohesion 是一个很重要的概念, 虽然聚合是有关结构概念, 但是聚合和 SRP 非常相关, 如前面论坛案例, 如果一个类不代表一个高聚合, 意味着低凝聚 low Cohesion, 它就可能意味破坏 SRP. 一个低凝聚的特点: 一个类有两个字段, 其中一个字段被一些方法使用;另外一个字段被其他方法使用.
在一个地方改动影响另外一个地方: 如果在一个代码地方加入新功能或只是简单重构, 却影响了其他不相关的地方, 意味着这个地方代码可能破坏了SRP.
猎枪效果Shotgun Effect: 如果一个小的改变引起一发动全身, 这意味 SRP 被破坏了.
不能够封装模块: 比如使用 Spring 框架, 你使用 @Configuration or XML 配置, 如果你不能在一个配置中封装一个 Bean 意味着它有太多职责. Spring配置应该隐藏内部bean, 暴露最少接口, 如果你因为多个原因需要改变 Spring 配置, 可能破坏了 SRP.
示例
反例
class Modem {
void dial(String pno); // 拨号
void hangup(); // 挂断
void send(char c); // 发送数据
char recv(); // 接收数据
};
乍一看似乎没有什么, 但是事实上 Modem
类承担了连接管理和数据管理两部分. 而这两个部分实际上是没有关联的.
正例
#ifndef DIALER_H
#define DIALER_H
#include <string>
class Dialer {
public:
void dial(const std::string& phoneNumber) {
// 执行拨号操作
}
void hangup() {
// 执行挂断操作
}
};
#endif
#ifndef CONNECTIONMANAGER_H
#define CONNECTIONMANAGER_H
#include "Dialer.h"
class ConnectionManager {
private:
Dialer dialer;
public:
void dial(const std::string& phoneNumber) {
dialer.dial(phoneNumber);
}
void hangup() {
dialer.hangup();
}
};
#endif
#ifndef DATASENDER_H
#define DATASENDER_H
class DataSender {
public:
void send(char c) {
// 执行发送数据操作
}
};
#endif
#ifndef DATARECEIVER_H
#define DATARECEIVER_H
class DataReceiver {
public:
char recv() {
// 执行接收数据操作
return 'a'; // 假设接收到字符 'a'
}
};
#endif
#ifndef DATATRANSFERMANAGER_H
#define DATATRANSFERMANAGER_H
#include "DataSender.h"
#include "DataReceiver.h"
class DataTransferManager {
private:
DataSender sender;
DataReceiver receiver;
public:
void send(char c) {
sender.send(c);
}
char recv() {
return receiver.recv();
}
};
#endif
总结
该原则的核心在于掌握代码组织的粒度. 尽可能地缩小这个粒度, 以此增加代码的复用性以及代码变更的影响范围.