什么是单一职责?
关于单一职责,看过很多版本的解释,这里归纳最常见的三个版本:
- 版本一:一个类只有一个引起变化的原因
- 版本二:一个类都应该只负责一项职责
- 版本三:一个类只能干一件事情
哪个版本的解释比较合理呢?
单一职责原则,英文是:Single responsibility principle(SRP),是 Robert C. Martin提出的 SOLID原则中的一种,所以,我们先看看 作者对单一职责原则的描述,这里摘取了作者关于单一职责的原文:
The Single Responsibility Principle (SRP) states that
each software module should have one and only one reason to change.
原文翻译为:单一职责原则指出,任何一个软件模块都应该有一个且只有一个修改的理由。
定义看起来很严谨,但似乎和现实是相冲突的,因为软件设计本身就是一门关注长期变化的学问,变化是软件中最常见不过的问题,在现实环境中,软件系统为了满足用户和所有者的要求,势必会作出各种修改,而系统的用户或者所有者就是该设计原则所指的"被修改的原因"。
于是乎,作者又重新把单一职责描述为:
The single responsibility principle states that every module
or class should have responsibility over a single part of
the functionality provided by the software, and that
responsibility should be entirely encapsulated by the class.
原文翻译为:单一职责原则指出,每个模块或类应该只负责软件所提供功能的一部分,并且这个职责应该完全被该类封装。
在这个定义中,每个模块或者类只负责软件的一部分功能,那这一部分是多少呢?这部分功能是否可以包含不同类型的行为呢?比如,电商中的订单和物流都可以叫做电商的一部分功能,但是他们在业务意义上显然是不同的领域,因此,该定义缺乏了定性。
于是乎,作者再次修改了单一职责的定义:
Each module should only be responsible to one actor.
原文翻译为:任何一个软件模块都应该只对某一类行为者负责
这个定义,只要是能归结成一类的行为,都可以属于某个模块的功能,这样定义看起来更符合现实业务的语意。
软件模块是什么?
在上述单一职责几个定义中都提到了软件模块,那么,软件模块到底是什么呢?
软件模块(Software Module)是指软件系统中的一个独立单元,它包含一组相关的功能和数据,这些模块是通过封装数据和功能来实现的,以便实现更高的代码复用性、可维护性和可扩展性。通常具有以下特点:
- 独立性:模块是相对独立的代码单元,可以单独开发、测试和部署。模块的独立性提高了系统的灵活性,使得各个模块可以独立演化和更新,而不影响其他模块。
- 封装性:模块内部的数据和实现细节对外界隐藏,只通过公开的接口与其他模块进行交互。封装性提高了代码的安全性和可维护性。
- 职责单一:每个模块通常只负责一组相关的功能,这有助于遵循单一职责原则,使得模块更加易于理解和维护。
- 可重用性:模块设计得当,可以在不同的项目中重复使用,提高了开发效率和代码质量。
- 可替换性:模块通过标准化的接口与外界交互,可以在不影响其他部分的前提下替换或更新某个模块。
为了更好地说明软件模块,这里以一个电商系统为例,它可能包含以下几个模块:
(1) 用户管理模块:
- 功能:处理用户的注册、登录、个人信息管理等。
- 接口:提供用户注册、登录、信息更新等服务。
(2) 订单管理模块:
- 功能:处理订单的创建、更新、查询等。
- 接口:提供订单创建、订单状态更新、订单查询等服务。
(3) 支付处理模块:
- 功能:处理订单的支付、退款等。
- 接口:提供支付请求、支付状态查询、退款等服务。
(4) 库存管理模块:
- 功能:处理商品的库存查询、更新等。
- 接口:提供库存查询、库存更新等服务。
单一职责示例
为了更好的说明任何一个软件模块都应该只对某一类行为者负责这个定义,下面我们通过2个 Java反例来进行演示。
反例1
假设有一个 Employee员工类并且包含以下 3个方法:
public class Employee {
// calculatePay() 实现计算员工薪酬
public Money calculatePay();
// save() 将Employee对象管理的数据存储到企业数据库中
public void save();
// postEvent() 用于促销活动发布
public void postEvent();
}
刚看上去,这个类设计得还挺符合实际业务,员工有计算薪酬、保存数据、发布促销等行为,但是这 3个方法对应三类不同的行为者,计算薪酬属于财务的行为,保存数据属于数据管理员的行为,发布促销属于销售的行为。
因此,Employee类将三类行为耦合在一起,违反了单一职责原则。假如一个普通员工不小心调用了calculatePay()方法,把每个员工的薪酬计算成了实际工资的2位,那可想而知这是一个灾难性的问题。
如果增加新需求,要求员工能够导出报表,因此,需要在 Employee类中增加了一个新的方法,代码如下:
// 导出报表
void exportReport();
接着需求又一个一个增加,Employee类就得一次一次的变动,这会导致什么结果呢?
一方面,Employee类会不断地膨胀;另一方面,可能业务需求完全不同,却始终需要在同一个 Employee类上改动,合理吗?
联想一下你的日常开发,是否也有这样的设计?把很多不同的行为都耦合到同一个类中,然后随着业务的增加,该类急剧膨胀,最后无法维护。
该如何解决这种问题呢?
解决这个问题的方法有很多,特定的行为只能由特定的行为者来操作,因此,需要把 Employee类拆解成 3种行为者(财务、数据管理员、销售),Employee类拆分之后的代码如下:
// 财务行为
public class FinanceStaff {
public Money calculatePay();
}
// 数据管理员行为
public class TechnicalStaff {
public void save();
}
// 销售行为
public class OperatorStaff {
public String postEvent();
}
反例2
假设需要开发一个电商系统,其中有一个 Order订单类,负责处理订单的创建、订单的支付以及订单的通知,代码如下:
public class Order {
public void createOrder() {
// 订单创建逻辑
}
public void processPayment() {
// 支付处理逻辑
}
public void sendNotification() {
// 发送通知逻辑
}
}
在上述代码中,Order类同时承担了订单创建、支付处理和通知发送的职责,违反了单一职责原则,因为一个类有多个引起变化的原因。
为了遵循SRP,我们需要将不同的职责分离到不同的类中,因此可以创建三个类:Order类负责订单创建,PaymentProcessor类负责支付处理,NotificationService类负责通知发送,每个类都只承担一个职责,从而遵循了单一职责原则。代码如下:
public class Order {
public void createOrder() {
// 订单创建逻辑
}
}
public class PaymentProcessor {
public void processPayment(Order order) {
// 支付处理逻辑
}
}
public class NotificationService {
public void sendNotification(Order order) {
// 发送通知逻辑
}
}
上面2个示例代码的拆分都遵从了原则:因相同原因而发生变化的事物聚集在一起,因不同原因而改变的事物分开。这就是单一职责的真正体现,也是定义内聚和耦合的一种方式。
总结
从作者 Robert C. Martin对单一职责的 3次定义变更,我们可以看出:
- 单一职责原则本质上就是要理解分离关注点。
- 单一职责原则可以应用于不同的层次,小到一个函数,大到一个系统。
- 软件设计也不可能一成不变。
回归到实际的工作中,我们可以把一个系统模块看作一个单一职责的行为者,比如:订单系统只关注订单相关的行为,交易系统只关注交易相关的行为,我们也可以把类作为一个单一职责的行为者,比如:订单类,把订单相关的 CRUD聚合在一起,支付类,把支付相关的信息聚合在一起。
因此,任何一个软件模块都应该只对某一类行为者负责这个定义才更适合单一职责。
最后,单一职责原则是面向对象设计的重要原则之一,它可以提高代码的可维护性、可读性和可扩展性,在日常开发中,遵循 SRP可以有效地降低类之间的耦合度,提高系统的稳定性和灵活性,从而写出更高质量的代码。