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