1.1 单一职责原则 (Single Responsibility Principle, SRP)

158 阅读8分钟

1.1 单一职责原则 (Single Responsibility Principle, SRP)

核心定义

一个类应该只有一个引起变化的原因,即一个类只负责一项职责。该原则强调将不同的功能分离到不同的类中,避免出现所谓的“上帝类”(God Class)——一个类承担了过多的职责,导致其变得臃肿、难以理解和修改。

深层解读与目的

SRP的核心在于“职责”的划分。这里的职责可以理解为“变化的原因”。如果一个类承担了多个职责,那么当其中一个职责发生变化时,可能会影响到其他职责的实现,从而增加了代码修改的风险和复杂性。

  • 降低复杂度:每个类只关注一个特定的功能点,使得类的设计和实现更加简单明了。
  • 提高内聚性:类内部的方法和数据都围绕着单一的职责紧密组织。
  • 减少耦合:当一个类的职责单一时,它与其他类的依赖关系也会相对简单。
  • 增强可读性与可维护性:职责清晰的类更容易被理解,修改一个功能时,影响范围也更小,不易引入新的错误。
  • 提高复用性:功能单一、职责明确的类更容易在其他地方被复用。

生活化类比

  1. 厨房工具:一把菜刀专门用于切菜,一个平底锅专门用于煎炸。如果一把工具试图同时具备切、砍、削、锯、煎、炒、烹、炸所有功能,那它可能会变得非常笨重且每项功能都做得不好。专业的厨师会为不同的任务选择专门的工具。
  2. 餐厅员工分工:收银员负责收款和结账,服务员负责点餐和上菜,厨师负责烹饪食物,清洁工负责打扫卫生。如果让收银员同时承担烹饪和服务员的工作,不仅效率低下,而且容易出错。
  3. 瑞士军刀 vs. 专业工具箱:瑞士军刀虽然集成了多种功能,但在特定专业领域,如修理精密仪器,专业的螺丝刀、镊子等独立工具通常更有效。SRP倾向于后者,即每个“工具”(类)都有其明确的专业用途。

实际应用场景

  • 用户管理系统:一个User类如果既负责用户的属性(如用户名、密码),又负责用户的持久化(保存到数据库、从数据库读取),还负责用户信息的校验逻辑,就违反了SRP。可以将其拆分为:
    • User (POJO/Entity):只包含用户属性和基本getter/setter。
    • UserRepository:负责用户的数据库操作(CRUD)。
    • UserAuthenticatorUserService:负责用户认证、密码校验等业务逻辑。
  • 报告生成与数据处理:一个类如果既负责从数据源获取数据,又负责数据的复杂计算和转换,还负责将结果格式化为报告(如PDF、Excel),则职责过多。应拆分为数据获取类、数据处理/计算类、报告生成类。
  • UI与业务逻辑:在图形用户界面(GUI)应用中,一个界面类如果既处理用户输入事件、更新界面显示,又执行核心的业务计算和数据操作,就违反了SRP。应将业务逻辑分离到专门的服务类或逻辑处理类中。

作用与价值

作用维度具体表现
降低复杂度每个类的代码量减少,逻辑更集中,易于理解。
提高可读性职责清晰的类名和方法名使得代码意图更明确。
增强可维护性修改一个功能时,只需关注相关的少数类,减少了对系统其他部分的影响。
提高复用性职责单一的类更容易被其他模块或项目复用。
降低耦合度类之间的依赖关系更简单,一个类的变化不容易引起其他类的连锁反应。
易于测试职责单一的类更容易进行单元测试,因为其依赖和行为更可控。

代码示例 (Java)

违反SRP的例子:

// 违反SRP: UserSettingService 同时负责用户设置的获取、修改以及邮件通知
class UserSettingService_Bad {
    private String userId;

    public UserSettingService_Bad(String userId) {
        this.userId = userId;
    }

    public String getPreference(String key) {
        // 模拟从数据库获取用户偏好设置
        System.out.println("Getting preference '" + key + "' for user " + userId);
        return "value_for_" + key;
    }

    public void setPreference(String key, String value) {
        // 模拟向数据库保存用户偏好设置
        System.out.println("Setting preference '" + key + "' to '" + value + "' for user " + userId);
        // 假设设置成功后需要发送邮件通知
        sendEmailNotification("Preference Updated", "Your preference '" + key + "' has been updated.");
    }

    // 邮件通知功能,与用户设置的核心职责不同
    public void sendEmailNotification(String subject, String body) {
        System.out.println("Sending email to user " + userId + ": Subject='" + subject + "', Body='" + body + "'");
        // ... 实际的邮件发送逻辑 ...
    }
}

UserSettingService_Bad中,如果邮件发送逻辑需要修改(例如,更换邮件服务提供商,或修改邮件模板),会导致UserSettingService_Bad类发生变化,即使核心的用户设置存取逻辑并未改变。这表明它承担了多个引起变化的原因。

遵循SRP的例子:

// 职责1: 用户设置的存储和获取
class UserPreferenceRepository {
    private String userId;

    public UserPreferenceRepository(String userId) {
        this.userId = userId;
    }

    public String getPreference(String key) {
        System.out.println("Repository: Getting preference '" + key + "' for user " + userId);
        return "value_for_" + key;
    }

    public void setPreference(String key, String value) {
        System.out.println("Repository: Setting preference '" + key + "' to '" + value + "' for user " + userId);
    }
}

// 职责2: 邮件通知服务
class EmailService {
    public void sendEmail(String recipientId, String subject, String body) {
        System.out.println("EmailService: Sending email to user " + recipientId + ": Subject='" + subject + "', Body='" + body + "'");
        // ... 实际的邮件发送逻辑 ...
    }
}

// 协调类 (或者应用服务层)
class UserSettingService_Good {
    private UserPreferenceRepository preferenceRepository;
    private EmailService emailService;
    private String userId;

    public UserSettingService_Good(String userId, UserPreferenceRepository preferenceRepository, EmailService emailService) {
        this.userId = userId;
        this.preferenceRepository = preferenceRepository;
        this.emailService = emailService;
    }

    public String getPreference(String key) {
        return preferenceRepository.getPreference(key);
    }

    public void setPreference(String key, String value) {
        preferenceRepository.setPreference(key, value);
        // 业务流程中决定是否发送邮件
        emailService.sendEmail(this.userId, "Preference Updated", "Your preference '" + key + "' has been updated.");
    }
}

// 使用示例
// UserPreferenceRepository repo = new UserPreferenceRepository("user123");
// EmailService mailer = new EmailService();
// UserSettingService_Good service = new UserSettingService_Good("user123", repo, mailer);
// service.setPreference("theme", "dark");

在这个遵循SRP的例子中:

  • UserPreferenceRepository 只负责用户偏好设置的持久化逻辑。
  • EmailService 只负责发送邮件的逻辑。
  • UserSettingService_Good 负责协调这两个服务来完成业务流程,但它本身不直接实现持久化或邮件发送的细节。

现在,如果邮件发送逻辑需要修改,只需要改动EmailService。如果持久化方式需要改变,只需要改动UserPreferenceRepositoryUserSettingService_Good的职责是业务流程编排,相对稳定。

优缺点

优点缺点
降低了类的复杂度,提高了类的可读性。可能导致类的数量增加:将一个复杂的类拆分成多个小类,会增加系统中的类总数,有时可能会让系统结构在宏观上显得更分散。
提高了系统的可维护性。职责划分的主观性:“职责”的粒度有时难以界定,过度拆分可能导致类过于细碎,反而增加理解成本。
增强了类的内聚性,降低了耦合度。增加了类之间的通信成本:原本在一个类内部的方法调用,拆分后可能变成类与类之间的调用。
提高了代码的复用性。
变更风险降低。

最佳实践与应用指南

  1. 识别变化的原因:思考哪些变化会影响到这个类。如果一个类有多个独立的变化轴,那么它可能承担了多个职责。
  2. 职责的粒度要适中:职责划分得过细,会导致类数量剧增,增加系统的复杂性;划分得过粗,则达不到SRP的目的。需要根据具体业务场景和团队理解来权衡。
  3. “能用一句话清晰描述类的职责吗?”:如果不能,或者需要用“和”、“或”等连接词来描述多个不相关的功能,那么这个类可能违反了SRP。
  4. 关注点分离 (Separation of Concerns, SoC):SRP是SoC在类级别的一个体现。将不同的关注点分离到不同的模块或类中。
  5. 逐步演进:在项目初期,某些类可能因为简单而承担了略微多一点的职责。随着业务的发展和复杂性的增加,可以逐步对这些类进行重构,以更好地遵循SRP。
  6. 与业务领域对齐:类的职责划分应尽可能与业务领域的概念和流程保持一致,这样更容易被业务人员和开发人员共同理解。

单一职责原则是构建高质量软件的基础。虽然“职责”的界定需要经验和判断,但其核心思想——让每个类只做好一件事——对于创建清晰、可维护的系统至关重要。


添加公众号第一时间了解最新内容。

欢迎入群交流QQ:276097690