3架构整洁之道

单一职责原则

SRP

变更原因职责高内聚模块划分

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 类中:

java
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 的后果。

正确的做法是将这些职责分离到不同的类或模块中:

java
// 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 在不同粒度的应用

函数级别:

一个函数应该只做一件事,并且做好这件事。

java
// 违反 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

java
/**
 * 初始版本:典型的 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 生成

邮件发送

java
/**
 * 重构版本:职责分离
 */

// === 数据访问层 ===

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 应用中的职责分离

python
"""
展示 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 示例三:日志记录的职责分离

java
/**
 * 展示如何将日志记录这一横切关注点从业务逻辑中分离
 */

// === 日志抽象 ===

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 促进高内聚和低耦合,是良好设计的基础。但它也有应用边界,过度拆分会增加复杂性。关键在于找到平衡点,根据项目的规模和变化频率灵活应用。


八、习题

概念理解:解释为什么"一个类只做一件事"是一个模糊的定义,而"一个改变的理由"更加精确。给出具体例子说明。

职责识别:分析以下类有几个改变的理由,分别对应哪些行为主体:

java
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 导致过多的类,增加复杂性"。你同意这个观点吗?在什么情况下这种批评是合理的?如何回应?

3/12单一职责
0%