第3章 单一职责原则 - SRP (Single Responsibility Principle)
一、导读
1.1 本章学习目标
通过本章的学习,你将能够:
深入理解单一职责原则(SRP)的精确定义和核心思想
识别代码中违反 SRP 的模式和信号
掌握识别"行为主体"(Actor)和"改变理由"的方法
学会将混合职责的类重构为符合 SRP 的结构
理解 SRP 与内聚性、耦合度的关系
掌握在不同粒度(函数、类、组件、服务)应用 SRP 的技巧
理解 SRP 在分层架构和微服务中的应用
认识 SRP 的应用边界,避免过度拆分
1.2 为什么本章如此重要
单一职责原则是 SOLID 五个原则中最基础、也最容易被误解的一个。Uncle Bob 本人曾说,SRP 是关于设计原则中被最多误解的一个——很多人简单地将其理解为"一个类只做一件事",但这个"一件事"的定义模糊不清,导致要么拆分不足,要么过度拆分。
SRP 的真正价值在于它揭示了软件设计的本质:管理变化。软件系统之所以复杂,不是因为代码本身复杂,而是因为不同的利益相关者(行为主体)有不同的需求,这些需求以不同的方式、在不同的时间发生变化。SRP 提供了一种方法,将这些变化的影响隔离开来,使修改一个需求不会意外破坏其他需求。
在实际开发中,违反 SRP 是最常见的代码异味之一。God Class(上帝类)、Spaghetti Code(面条代码)等问题,根源往往是没有正确分离职责。理解并应用 SRP,可以显著改善代码的可维护性。
此外,SRP 是其他设计原则的基础。只有正确识别和分离了职责,才能有效地应用开闭原则、接口隔离原则等。可以说,SRP 是良好设计的起点。
1.3 核心问题引导
在深入本章内容之前,请思考以下问题:
"一个类只做一件事"中的"一件事"到底指什么?如何精确定义?
如何识别一个类承担了多个职责?有哪些信号?
职责分离的边界在哪里?拆分的粒度应该如何把握?
为什么混合职责会导致维护困难?具体的机制是什么?
如何识别系统中的"行为主体"(Actor)?
SRP 只适用于类级别吗?如何在函数、模块、服务级别应用?
过度拆分会带来什么问题?如何平衡?
在微服务架构中,SRP 如何指导服务边界的划分?
二、核心概念详解
2.1 SRP 的精确定义
单一职责原则的正式表述是:一个模块应该只有一个改变的理由(A module should have one, and only one, reason to change)。
这个定义中有两个关键概念需要澄清:
"模块"的含义:在这里,模块是一个广义的概念,可以指:
- 函数或方法
- 类或结构体
- 包或命名空间
- 组件或服务
- 任何代码组织单元
SRP 适用于所有层次的抽象,不仅仅是类。
"改变的理由"的含义:这是 SRP 的核心。改变的理由指的是来自不同行为主体(Actor)的需求变更。行为主体是指对系统有特定期望的利益相关者,可以是:
- 用户角色(如管理员、普通用户、审计员)
- 业务部门(如市场部、财务部、HR)
- 技术关注点(如安全性、性能、持久化)
- 外部系统(如支付网关、邮件服务)
如果一个模块服务于多个行为主体,那么每个行为主体的需求变更都可能成为修改这个模块的理由。这就违反了 SRP。
2.2 行为主体(Actor)分析
识别行为主体是应用 SRP 的关键步骤。行为主体代表了系统中不同的变化轴线。
考虑一个简单的员工管理系统:
行为主体分析:
- HR 部门:关心员工的基本信息(姓名、职位、部门)
- 财务部门:关心员工的薪资信息(工资、奖金、税)
- IT 部门:关心员工的系统访问权限(账号、角色、权限)
- 法务部门:关心员工的合同信息(入职日期、合同期限、保密协议)如果我们将所有这些信息放在一个 Employee 类中:
public class Employee {
// HR 关心的字段
private String name;
private String position;
private String department;
// 财务关心的字段
private double salary;
private double bonus;
private double taxRate;
// IT 关心的字段
private String username;
private Set<String> roles;
private Set<String> permissions;
// 法务关心的字段
private LocalDate hireDate;
private LocalDate contractEndDate;
private boolean signedNDA;
// 各种 getter/setter...
}这个类有四个改变的理由:
HR 可能要求添加新的员工属性(如紧急联系人)
财务可能要求更改薪资计算方式
IT 可能要求更改权限模型
法务可能要求添加新的合同条款
当 HR 要求添加一个新字段时,理论上可能影响到财务、IT、法务相关的代码。这就是违反 SRP 的后果。
正确的做法是将这些职责分离到不同的类或模块中:
// HR 关心的员工信息
public class EmployeeProfile {
private String name;
private String position;
private String department;
// 只包含 HR 相关的操作
}
// 财务关心的薪资信息
public class CompensationRecord {
private double salary;
private double bonus;
private double taxRate;
// 只包含薪资相关的操作
}
// IT 关心的访问信息
public class AccessCredentials {
private String username;
private Set<String> roles;
private Set<String> permissions;
// 只包含访问控制相关的操作
}
// 法务关心的合同信息
public class EmploymentContract {
private LocalDate hireDate;
private LocalDate contractEndDate;
private boolean signedNDA;
// 只包含合同相关的操作
}现在,每个类只有一个改变的理由,分别对应一个行为主体。
2.3 职责混合的危害
混合职责会导致一系列问题:
1. 意外的副作用
当修改一个职责时,可能意外影响其他职责。例如,在一个同时处理业务逻辑和数据持久化的类中,修改业务规则可能导致数据库查询出错。
2. 合并冲突
当多个开发者同时修改同一个类的不同部分时,版本控制系统会产生合并冲突。即使修改的是不相关的功能,也需要手动解决冲突。
3. 测试困难
测试一个职责可能需要设置其他职责的依赖。例如,测试业务逻辑可能需要配置数据库连接,即使测试本身不涉及数据持久化。
4. 理解成本高
阅读代码时需要理解所有职责的逻辑,即使只关心其中一个。这增加了认知负担。
5. 部署耦合
如果职责涉及不同的部署关注点(如业务逻辑和安全策略),它们被绑在一起意味着无法独立更新。
2.4 识别职责的信号
以下是一些识别职责混合的信号:
代码层面的信号:
- 类名中包含"And"、"Or"、"Manager"、"Processor"等泛化词汇
- 类中有明显分组的一组字段和方法
- 某些方法只被部分其他方法使用
- 存在大量私有方法,可以分为几个逻辑组
- 构造函数参数过多,涉及不同类型的关注点
变更层面的信号:
- 不同的人或团队经常修改同一个文件的不同部分
- 修改某个功能时,经常意外破坏看似无关的功能
- 代码审查时, reviewers 经常说"这部分我不熟悉,让 XX 来看"
测试层面的信号:
- 单元测试需要设置大量不相关的 fixture
- 测试用例覆盖了多种不同的场景,难以归类
- Mock 对象种类繁多,涉及不同的关注点
2.5 职责分离的策略
识别出混合职责后,有多种策略可以分离它们:
1. 提取类(Extract Class)
将相关的字段和方法提取到一个新类中。这是最常见的重构手法。
2. 提取接口(Extract Interface)
如果客户端只需要部分功能,可以定义一个只包含所需方法的接口。
3. 引入委托(Introduce Delegation)
将部分职责委托给另一个专门的类。
4. 使用组合而非继承
通过组合多个小类来构建功能,而不是创建一个大的继承层次。
5. 分层架构
将不同层次的职责(表现层、业务层、数据层)分离到不同的包或模块中。
2.6 SRP 在不同粒度的应用
函数级别:
一个函数应该只做一件事,并且做好这件事。
// 违反 SRP
public void processOrder(Order order) {
validate(order); // 验证
calculateTotal(order); // 计算
saveToDatabase(order); // 持久化
sendEmail(order); // 通知
}
// 符合 SRP - 每个函数做一件事
public void processOrder(Order order) {
OrderValidator.validate(order);
double total = PricingService.calculateTotal(order);
order.setTotal(total);
orderRepository.save(order);
notificationService.sendConfirmation(order);
}类级别:
一个类应该只有一个改变的理由,对应一个行为主体。
组件/模块级别:
一个组件应该封装一组相关的功能,对外提供清晰的接口。
服务级别(微服务):
一个微服务应该围绕一个业务能力或业务领域构建,对应一个限界上下文。
2.7 SRP 与内聚性、耦合度的关系
SRP 与两个经典的软件设计度量密切相关:
内聚性(Cohesion):衡量一个模块内部元素之间的关联程度。高内聚意味着模块内的元素紧密相关,共同完成一个明确的任务。SRP 促进高内聚,因为它要求模块专注于单一职责。
耦合度(Coupling):衡量模块之间的依赖程度。低耦合意味着模块之间相互独立,修改一个模块不太可能影响其他模块。SRP 通过分离职责,减少了不必要的耦合。
理想的设计是高内聚、低耦合。SRP 是实现这一目标的重要手段。
三、程序员的视角
3.1 在实际项目中的应用
在日常工作中,应用 SRP 可以从以下几个方面入手:
代码审查时:
- 检查类的大小和方法的数量
- 寻找可以分组的字段和方法
- 询问"这个类有几个改变的理由?"
- 注意 God Class 的迹象
设计新功能时:
- 首先识别涉及的行为主体
- 为每个行为主体设计独立的模块
- 明确模块之间的交互方式
- 避免将不同主体的逻辑混在一起
重构遗留代码时:
- 从最大的类开始分析
- 识别自然形成的职责分组
- 逐步提取,每次提取后进行测试
- 不要一次性大规模重构
3.2 常见陷阱与误区
陷阱一:过度拆分
有些人认为越小越好,将一个简单的类拆分成十几个微小的类。这增加了理解的复杂度,因为读者需要在多个文件之间跳转。
陷阱二:机械地按技术层次拆分
将所有 DAO 放一起、所有 Service 放一起、所有 Controller 放一起,这只是技术上的分组,不一定符合 SRP。SRP 关注的是业务职责,不是技术实现。
陷阱三:忽视隐式职责
日志记录、异常处理、事务管理等横切关注点也是职责,应该适当分离(如使用 AOP),而不是散布在业务代码中。
陷阱四:混淆"相关"和"同一职责"
相关的功能不一定属于同一职责。例如,订单计算和订单通知都涉及订单,但它们服务于不同的行为主体(财务 vs 客服),应该分离。
3.3 与其他层级的联系
SRP 与其他设计原则和模式密切相关:
- OCP:单一职责的类更容易扩展,因为扩展点更清晰
- ISP:识别职责自然导致接口的拆分
- DDD:限界上下文是 SRP 在领域层面的应用
- 微服务:服务边界应该基于业务能力(职责)划分
- Clean Architecture:各层有明确的职责分工
四、代码实战
4.1 示例一:重构 God Class
/**
* 初始版本:典型的 God Class,违反 SRP
*/
public class ReportGenerator {
// 数据访问相关
private Connection dbConnection;
// 报告数据
private List<ReportRow> data;
// 格式化相关
private String dateFormat = "yyyy-MM-dd";
private String currencySymbol = "$";
// 输出相关
private OutputStream outputStream;
public ReportGenerator() throws SQLException {
this.dbConnection = DriverManager.getConnection("jdbc:mysql://localhost/reporting");
this.outputStream = new FileOutputStream("report.pdf");
}
// 数据获取逻辑
public void loadData(String query) throws SQLException {
Statement stmt = dbConnection.createStatement();
ResultSet rs = stmt.executeQuery(query);
this.data = new ArrayList<>();
while (rs.next()) {
data.add(new ReportRow(
rs.getString("name"),
rs.getDouble("amount"),
rs.getDate("date")
));
}
rs.close();
stmt.close();
}
// 数据过滤逻辑
public void filterByDateRange(LocalDate start, LocalDate end) {
data.removeIf(row -> row.getDate().isBefore(start) || row.getDate().isAfter(end));
}
public void filterByAmount(double minAmount) {
data.removeIf(row -> row.getAmount() < minAmount);
}
// 计算逻辑
public double calculateTotal() {
return data.stream().mapToDouble(ReportRow::getAmount).sum();
}
public double calculateAverage() {
if (data.isEmpty()) return 0;
return calculateTotal() / data.size();
}
// PDF 生成逻辑
public void generatePdf() throws IOException {
// 复杂的 PDF 生成代码...
outputStream.write("%PDF-1.4".getBytes());
// ... 更多 PDF 生成逻辑
}
// HTML 生成逻辑
public void generateHtml() throws IOException {
StringBuilder html = new StringBuilder();
html.append("<html><body>");
html.append("<h1>Report</h1>");
html.append("<table>");
for (ReportRow row : data) {
html.append("<tr>");
html.append("<td>").append(row.getName()).append("</td>");
html.append("<td>").append(currencySymbol).append(row.getAmount()).append("</td>");
html.append("<td>").append(formatDate(row.getDate())).append("</td>");
html.append("</tr>");
}
html.append("</table>");
html.append("</body></html>");
Files.writeString(Path.of("report.html"), html.toString());
}
// CSV 生成逻辑
public void generateCsv() throws IOException {
StringBuilder csv = new StringBuilder();
csv.append("Name,Amount,Date\n");
for (ReportRow row : data) {
csv.append(row.getName()).append(",");
csv.append(row.getAmount()).append(",");
csv.append(formatDate(row.getDate())).append("\n");
}
Files.writeString(Path.of("report.csv"), csv.toString());
}
// 邮件发送逻辑
public void emailReport(String recipient) throws MessagingException {
Session session = Session.getDefaultInstance(new Properties());
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress("reports@company.com"));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(recipient));
message.setSubject("Monthly Report");
message.setText("Please find the attached report.");
// 添加附件
MimeBodyPart attachmentPart = new MimeBodyPart();
DataSource source = new FileDataSource("report.pdf");
attachmentPart.setDataHandler(new DataHandler(source));
attachmentPart.setFileName("report.pdf");
MimeMultipart multipart = new MimeMultipart();
multipart.addBodyPart(attachmentPart);
message.setContent(multipart);
Transport.send(message);
}
private String formatDate(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern(dateFormat));
}
}这个类承担了太多职责:
数据库连接管理
数据加载
数据过滤
数据计算
PDF 生成
HTML 生成
CSV 生成
邮件发送
/**
* 重构版本:职责分离
*/
// === 数据访问层 ===
public interface ReportDataSource {
List<ReportRow> loadData(String query);
}
public class JdbcReportDataSource implements ReportDataSource {
private final DataSource dataSource;
public JdbcReportDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public List<ReportRow> loadData(String query) {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query)) {
List<ReportRow> rows = new ArrayList<>();
while (rs.next()) {
rows.add(new ReportRow(
rs.getString("name"),
rs.getDouble("amount"),
rs.getDate("date").toLocalDate()
));
}
return rows;
} catch (SQLException e) {
throw new DataAccessException("Failed to load report data", e);
}
}
}
// === 数据过滤层 ===
public class ReportFilter {
public List<ReportRow> filterByDateRange(List<ReportRow> rows, LocalDate start, LocalDate end) {
return rows.stream()
.filter(row -> !row.getDate().isBefore(start) && !row.getDate().isAfter(end))
.collect(Collectors.toList());
}
public List<ReportRow> filterByAmount(List<ReportRow> rows, double minAmount) {
return rows.stream()
.filter(row -> row.getAmount() >= minAmount)
.collect(Collectors.toList());
}
public List<ReportRow> applyFilters(List<ReportRow> rows, ReportFilterCriteria criteria) {
List<ReportRow> result = rows;
if (criteria.hasDateRange()) {
result = filterByDateRange(result, criteria.getStartDate(), criteria.getEndDate());
}
if (criteria.hasMinAmount()) {
result = filterByAmount(result, criteria.getMinAmount());
}
return result;
}
}
// === 计算层 ===
public class ReportCalculator {
public double calculateTotal(List<ReportRow> rows) {
return rows.stream().mapToDouble(ReportRow::getAmount).sum();
}
public double calculateAverage(List<ReportRow> rows) {
if (rows.isEmpty()) return 0;
return calculateTotal(rows) / rows.size();
}
public Map<String, Double> calculateByCategory(List<ReportRow> rows) {
return rows.stream()
.collect(Collectors.groupingBy(
ReportRow::getCategory,
Collectors.summingDouble(ReportRow::getAmount)
));
}
public ReportSummary generateSummary(List<ReportRow> rows) {
return new ReportSummary(
rows.size(),
calculateTotal(rows),
calculateAverage(rows),
calculateByCategory(rows)
);
}
}
// === 格式化层 ===
public interface ReportFormatter {
byte[] format(List<ReportRow> rows, ReportSummary summary);
String getFileExtension();
}
public class PdfReportFormatter implements ReportFormatter {
@Override
public byte[] format(List<ReportRow> rows, ReportSummary summary) {
// PDF 生成逻辑
return new byte[0]; // 简化
}
@Override
public String getFileExtension() {
return "pdf";
}
}
public class HtmlReportFormatter implements ReportFormatter {
private final String currencySymbol;
public HtmlReportFormatter(String currencySymbol) {
this.currencySymbol = currencySymbol;
}
@Override
public byte[] format(List<ReportRow> rows, ReportSummary summary) {
StringBuilder html = new StringBuilder();
html.append("<html><head><style>")
.append("table { border-collapse: collapse; width: 100%; }")
.append("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }")
.append("</style></head><body>");
html.append("<h1>Report</h1>");
html.append("<p>Total: ").append(currencySymbol)
.append(String.format("%.2f", summary.getTotal())).append("</p>");
html.append("<table><tr><th>Name</th><th>Amount</th><th>Date</th></tr>");
for (ReportRow row : rows) {
html.append("<tr>")
.append("<td>").append(row.getName()).append("</td>")
.append("<td>").append(currencySymbol).append(String.format("%.2f", row.getAmount())).append("</td>")
.append("<td>").append(row.getDate()).append("</td>")
.append("</tr>");
}
html.append("</table></body></html>");
return html.toString().getBytes(StandardCharsets.UTF_8);
}
@Override
public String getFileExtension() {
return "html";
}
}
public class CsvReportFormatter implements ReportFormatter {
@Override
public byte[] format(List<ReportRow> rows, ReportSummary summary) {
StringBuilder csv = new StringBuilder();
csv.append("Name,Amount,Date,Category\n");
for (ReportRow row : rows) {
csv.append(row.getName()).append(",")
.append(row.getAmount()).append(",")
.append(row.getDate()).append(",")
.append(row.getCategory()).append("\n");
}
return csv.toString().getBytes(StandardCharsets.UTF_8);
}
@Override
public String getFileExtension() {
return "csv";
}
}
// === 输出层 ===
public interface ReportOutput {
void output(byte[] content, String fileName) throws IOException;
}
public class FileOutput implements ReportOutput {
private final Path outputDirectory;
public FileOutput(Path outputDirectory) {
this.outputDirectory = outputDirectory;
}
@Override
public void output(byte[] content, String fileName) throws IOException {
Path filePath = outputDirectory.resolve(fileName);
Files.write(filePath, content);
}
}
public class EmailOutput implements ReportOutput {
private final EmailService emailService;
private final String recipient;
public EmailOutput(EmailService emailService, String recipient) {
this.emailService = emailService;
this.recipient = recipient;
}
@Override
public void output(byte[] content, String fileName) throws IOException {
emailService.sendWithAttachment(recipient, "Report", "Please find the attached report.", fileName, content);
}
}
// === 协调层(Facade)===
public class ReportService {
private final ReportDataSource dataSource;
private final ReportFilter filter;
private final ReportCalculator calculator;
public ReportService(ReportDataSource dataSource, ReportFilter filter, ReportCalculator calculator) {
this.dataSource = dataSource;
this.filter = filter;
this.calculator = calculator;
}
public byte[] generateReport(String query, ReportFilterCriteria criteria, ReportFormatter formatter) {
// 加载数据
List<ReportRow> data = dataSource.loadData(query);
// 应用过滤
List<ReportRow> filteredData = filter.applyFilters(data, criteria);
// 计算汇总
ReportSummary summary = calculator.generateSummary(filteredData);
// 格式化输出
return formatter.format(filteredData, summary);
}
}
// === 使用示例 ===
public class ReportApplication {
public static void main(String[] args) throws Exception {
// 设置依赖
DataSource ds = setupDataSource();
ReportDataSource dataSource = new JdbcReportDataSource(ds);
ReportFilter filter = new ReportFilter();
ReportCalculator calculator = new ReportCalculator();
ReportService reportService = new ReportService(dataSource, filter, calculator);
// 生成 HTML 报告并保存到文件
ReportFilterCriteria criteria = new ReportFilterCriteria.Builder()
.dateRange(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 12, 31))
.minAmount(100.0)
.build();
byte[] htmlContent = reportService.generateReport(
"SELECT * FROM sales",
criteria,
new HtmlReportFormatter("$")
);
ReportOutput fileOutput = new FileOutput(Path.of("/tmp/reports"));
fileOutput.output(htmlContent, "sales_report.html");
// 生成 PDF 并通过邮件发送
byte[] pdfContent = reportService.generateReport(
"SELECT * FROM sales",
criteria,
new PdfReportFormatter()
);
EmailService emailService = new SmtpEmailService("smtp.company.com");
ReportOutput emailOutput = new EmailOutput(emailService, "manager@company.com");
emailOutput.output(pdfContent, "sales_report.pdf");
}
}4.2 示例二:Web 应用中的职责分离
"""
展示 Web 应用中如何应用 SRP 分离职责
"""
from abc import ABC, abstractmethod
from typing import List, Optional
from dataclasses import dataclass
from datetime import datetime
import json
# === 模型层(数据表示)===
@dataclass
class User:
id: int
username: str
email: str
created_at: datetime
@dataclass
class CreateUserRequest:
username: str
email: str
password: str
@dataclass
class ApiResponse:
status_code: int
body: dict
headers: dict = None
# === 验证层 ===
class ValidationRule(ABC):
@abstractmethod
def validate(self, value: str) -> Optional[str]:
"""返回错误消息,如果验证通过则返回 None"""
pass
class UsernameValidationRule(ValidationRule):
def validate(self, username: str) -> Optional[str]:
if not username:
return "Username is required"
if len(username) < 3:
return "Username must be at least 3 characters"
if not username.isalnum():
return "Username can only contain letters and numbers"
return None
class EmailValidationRule(ValidationRule):
def validate(self, email: str) -> Optional[str]:
if not email:
return "Email is required"
if "@" not in email:
return "Invalid email format"
if len(email) > 254:
return "Email is too long"
return None
class PasswordValidationRule(ValidationRule):
def validate(self, password: str) -> Optional[str]:
if not password:
return "Password is required"
if len(password) < 8:
return "Password must be at least 8 characters"
if not any(c.isupper() for c in password):
return "Password must contain at least one uppercase letter"
if not any(c.isdigit() for c in password):
return "Password must contain at least one digit"
return None
class RequestValidator:
def __init__(self, rules: List[ValidationRule]):
self.rules = rules
def validate(self, value: str) -> List[str]:
errors = []
for rule in self.rules:
error = rule.validate(value)
if error:
errors.append(error)
return errors
# === 密码加密层 ===
class PasswordHasher(ABC):
@abstractmethod
def hash(self, password: str) -> str:
pass
@abstractmethod
def verify(self, password: str, hashed: str) -> bool:
pass
class BcryptPasswordHasher(PasswordHasher):
def hash(self, password: str) -> str:
# 实际实现会使用 bcrypt
return f"hashed_{password}"
def verify(self, password: str, hashed: str) -> bool:
return hashed == f"hashed_{password}"
# === 数据访问层 ===
class UserRepository(ABC):
@abstractmethod
def find_by_username(self, username: str) -> Optional[User]:
pass
@abstractmethod
def find_by_email(self, email: str) -> Optional[User]:
pass
@abstractmethod
def save(self, user: User) -> User:
pass
class InMemoryUserRepository(UserRepository):
def __init__(self):
self.users = {}
self.next_id = 1
def find_by_username(self, username: str) -> Optional[User]:
for user in self.users.values():
if user.username == username:
return user
return None
def find_by_email(self, email: str) -> Optional[User]:
for user in self.users.values():
if user.email == email:
return user
return None
def save(self, user: User) -> User:
self.users[user.id] = user
return user
# === 业务逻辑层 ===
class UserService:
def __init__(
self,
user_repo: UserRepository,
password_hasher: PasswordHasher,
username_validator: RequestValidator,
email_validator: RequestValidator,
password_validator: RequestValidator
):
self.user_repo = user_repo
self.password_hasher = password_hasher
self.username_validator = username_validator
self.email_validator = email_validator
self.password_validator = password_validator
def create_user(self, request: CreateUserRequest) -> User:
# 验证输入
username_errors = self.username_validator.validate(request.username)
email_errors = self.email_validator.validate(request.email)
password_errors = self.password_validator.validate(request.password)
all_errors = username_errors + email_errors + password_errors
if all_errors:
raise ValidationError(all_errors)
# 检查唯一性
if self.user_repo.find_by_username(request.username):
raise BusinessError("Username already exists")
if self.user_repo.find_by_email(request.email):
raise BusinessError("Email already registered")
# 创建用户
user = User(
id=0, # 将由 repository 分配
username=request.username,
email=request.email,
created_at=datetime.now()
)
saved_user = self.user_repo.save(user)
return saved_user
# === 表现层(Controller)===
class UserController:
def __init__(self, user_service: UserService):
self.user_service = user_service
def create_user(self, request_body: str) -> ApiResponse:
try:
# 解析请求
data = json.loads(request_body)
create_request = CreateUserRequest(
username=data.get("username", ""),
email=data.get("email", ""),
password=data.get("password", "")
)
# 调用业务逻辑
user = self.user_service.create_user(create_request)
# 构建响应
return ApiResponse(
status_code=201,
body={
"id": user.id,
"username": user.username,
"email": user.email,
"created_at": user.created_at.isoformat()
}
)
except ValidationError as e:
return ApiResponse(
status_code=400,
body={"error": "Validation failed", "details": e.errors}
)
except BusinessError as e:
return ApiResponse(
status_code=409,
body={"error": str(e)}
)
except Exception as e:
return ApiResponse(
status_code=500,
body={"error": "Internal server error"}
)
# === 自定义异常 ===
class ValidationError(Exception):
def __init__(self, errors: List[str]):
self.errors = errors
super().__init__(f"Validation failed: {errors}")
class BusinessError(Exception):
pass
# === 使用示例 ===
def main():
# 设置依赖
user_repo = InMemoryUserRepository()
password_hasher = BcryptPasswordHasher()
username_validator = RequestValidator([UsernameValidationRule()])
email_validator = RequestValidator([EmailValidationRule()])
password_validator = RequestValidator([PasswordValidationRule()])
user_service = UserService(
user_repo=user_repo,
password_hasher=password_hasher,
username_validator=username_validator,
email_validator=email_validator,
password_validator=password_validator
)
user_controller = UserController(user_service)
# 测试创建用户
request_body = json.dumps({
"username": "john_doe",
"email": "john@example.com",
"password": "SecurePass123"
})
response = user_controller.create_user(request_body)
print(f"Status: {response.status_code}")
print(f"Body: {response.body}")
if __name__ == "__main__":
main()4.3 示例三:日志记录的职责分离
/**
* 展示如何将日志记录这一横切关注点从业务逻辑中分离
*/
// === 日志抽象 ===
public interface Logger {
void debug(String message);
void info(String message);
void warn(String message);
void error(String message, Throwable t);
}
public class Slf4jLogger implements Logger {
private final org.slf4j.Logger logger;
public Slf4jLogger(Class<?> clazz) {
this.logger = org.slf4j.LoggerFactory.getLogger(clazz);
}
@Override
public void debug(String message) {
logger.debug(message);
}
@Override
public void info(String message) {
logger.info(message);
}
@Override
public void warn(String message) {
logger.warn(message);
}
@Override
public void error(String message, Throwable t) {
logger.error(message, t);
}
}
// 无操作日志器(用于测试或禁用日志的场景)
public class NoOpLogger implements Logger {
@Override
public void debug(String message) {}
@Override
public void info(String message) {}
@Override
public void warn(String message) {}
@Override
public void error(String message, Throwable t) {}
}
// === 业务逻辑(不包含日志代码)===
public class OrderCalculationService {
public double calculateTotal(Order order) {
double subtotal = order.getItems().stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
double discount = calculateDiscount(order);
return subtotal - discount;
}
private double calculateDiscount(Order order) {
if (order.getCustomer().isVip()) {
return order.getTotal() * 0.1;
}
return 0;
}
}
// === 使用装饰器或 AOP 添加日志 ===
public class LoggingOrderCalculationService implements OrderCalculationService {
private final OrderCalculationService delegate;
private final Logger logger;
public LoggingOrderCalculationService(OrderCalculationService delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
}
@Override
public double calculateTotal(Order order) {
logger.debug("Calculating total for order: " + order.getId());
try {
double result = delegate.calculateTotal(order);
logger.info("Calculated total: " + result + " for order: " + order.getId());
return result;
} catch (Exception e) {
logger.error("Failed to calculate total for order: " + order.getId(), e);
throw e;
}
}
}
// === 或者使用 Java 动态代理 ===
import java.lang.reflect.*;
public class LoggingProxy<T> implements InvocationHandler {
private final T target;
private final Logger logger;
public LoggingProxy(T target, Logger logger) {
this.target = target;
this.logger = logger;
}
@SuppressWarnings("unchecked")
public T createProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
logger.debug("Calling method: " + method.getName());
try {
Object result = method.invoke(target, args);
logger.debug("Method " + method.getName() + " completed successfully");
return result;
} catch (InvocationTargetException e) {
logger.error("Method " + method.getName() + " threw exception", e.getCause());
throw e.getCause();
}
}
}五、深入思考
5.1 历史背景
单一职责原则的思想可以追溯到 Larry Constantine 在 1970 年代提出的"耦合和内聚"概念。Robert C. Martin 在 2000 年代初将其形式化为 SRP,并纳入 SOLID 原则体系。
有趣的是,Uncle Bob 后来承认 SRP 是 SOLID 中最被误解的原则。许多人将其简化为"一个类只做一件事",但忽略了"一件事"的精确定义应该是"一个改变的理由",这与行为主体密切相关。
5.2 前沿方向
在现代软件架构中,SRP 的应用正在扩展到新的领域:
微服务边界:如何划分微服务的职责是一个挑战。领域驱动设计(DDD)中的限界上下文提供了指导。
Serverless 函数:每个函数应该有单一的职责,这促使更细粒度的函数设计。
前端组件:React/Vue 组件也应该遵循 SRP,区分展示组件和容器组件。
基础设施即代码:Terraform/CloudFormation 模板也应该按职责分离,如网络、计算、存储分开管理。
5.3 延伸阅读
- 《Clean Code》第 3 章:函数层面的 SRP
- 《Refactoring》中的 Extract Class 重构手法
- 《Domain-Driven Design》中的限界上下文概念
- 《Building Microservices》中的服务拆分策略
六、实验笔记
实验一:识别和重构混合职责
目标:在一个真实项目中识别违反 SRP 的代码并进行重构。
步骤:
选择一个你熟悉的项目
找出最大的 5 个类
对每个类进行职责分析:
- 列出所有字段和方法
- 尝试将它们分组为不同的职责
- 识别对应的行为主体
选择一个类进行重构:
- 设计新的类结构
- 提取职责到独立的类
- 确保所有测试通过
比较重构前后的代码质量指标
预期结果:
- 职责分析报告
- 重构后的代码
- 前后对比数据
实验二:设计符合 SRP 的新功能
目标:从零开始设计一个功能,确保从一开始就遵循 SRP。
任务:设计一个简单的博客系统的文章管理功能。
步骤:
识别行为主体(作者、编辑、读者、管理员等)
为每个主体设计独立的模块
定义模块之间的接口
实现核心功能
演示如何添加新功能而不修改现有代码
反思问题:
- SRP 是否使设计更清晰?
- 有没有遇到难以分离的职责?
- 拆分后是否有过多的类?
七、本章小结
本章深入探讨了单一职责原则(SRP),这是 SOLID 原则的基础。我们学习了:
SRP 的精确定义是"一个模块应该只有一个改变的理由",其中"改变的理由"对应于不同的行为主体(Actor)。行为主体是对系统有不同期望的利益相关者,如用户角色、业务部门或技术关注点。
混合职责会导致意外的副作用、合并冲突、测试困难和理解成本增加。识别职责混合的信号包括类名泛化、方法分组、多人修改同一文件等。
分离职责的策略包括提取类、提取接口、引入委托、使用组合和分层架构。SRP 适用于所有粒度,从函数到服务。
SRP 促进高内聚和低耦合,是良好设计的基础。但它也有应用边界,过度拆分会增加复杂性。关键在于找到平衡点,根据项目的规模和变化频率灵活应用。
八、习题
概念理解:解释为什么"一个类只做一件事"是一个模糊的定义,而"一个改变的理由"更加精确。给出具体例子说明。
职责识别:分析以下类有几个改变的理由,分别对应哪些行为主体:
public class CustomerManager {
private String name;
private String email;
private String address;
private double creditLimit;
private String paymentTerms;
private String username;
private String passwordHash;
private List<String> permissions;
}重构练习:将上述 CustomerManager 类重构为符合 SRP 的结构。画出新的类图。
行为主体分析:为一个在线书店系统识别至少 5 个行为主体,并说明每个主体关心的职责。
权衡分析:在什么情况下,将两个职责放在同一个类中可能是合理的?给出具体的场景和理由。
代码审查:审查你最近编写的代码,找出违反 SRP 的地方。描述如何改进。
微服务应用:如何应用 SRP 指导微服务的边界划分?以一个电商系统为例,说明服务应该如何拆分。
前端应用:在 React/Vue 组件设计中,如何应用 SRP?区分展示组件和容器组件的好处是什么?
研究题:调研"命令查询职责分离"(CQRS)模式,它与 SRP 有什么关系?
批判性思考:有人说"SRP 导致过多的类,增加复杂性"。你同意这个观点吗?在什么情况下这种批评是合理的?如何回应?