1、单一职责原则(Single Responsibility Principle)

核心思想

定义
A class should have only one reason to change. ( 就一个类而言,应该仅有一个引起它变化的原因)

每一个职责都是变化的一个轴线(an axis of change)。当需求变化时,该变化会反映为类的职责变化。如果一个类承担了多于一个的职责,那么引起它变化的原因就会有多个。

如果一个类承担的职责过多,就等于把这些职责耦合在了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生是,设计会遭到意想不到的破坏。

引用自:《Agile Software Development, Principles, Patterns, and Practices》

关于这句话中的 “类的变化” 是指什么,书中并没有给出明确的说明。从字面上来理解的话应该是指类的代码的修改。

那么单一职责的作用是什么呢?我认为主要有以下几点:

  1. 降低类的复杂度
  2. 使代码的可读性和可维护性提高。
  3. 降低业务的耦合度,使某项业务修改时只需要关注一个点,而不是整个业务链。(后面举例说明)

划分职责的边界

通过下面两个实例,我们来学习如何划分责任的边界。

以下是我个人对职责的边界的认识,如有不恰当的地方,欢迎批评指正。

实例1:用户信息维护类

看似非单一职责,实则是单一职责。

比如存在一个用户信息维护类,假设这个类的每个方法都是很简单的业务。

单一指责反例1

这时候怎么划分职责呢?

我们可以说这个类的职责是维护用户信息。我们也可以说这个类担任了用户信息查询、用户添加、用户信息更新、用户删除,4个职责。如果按照每种方法代表一种职责的话,这个简单的类将被拆分为4个更简单的类,这样是满足SRP,但是其实这样做是很不利于我们维护代码的。而且拆分之后并没有降低类的复杂度,因为这个类本来就不复杂。所以这里我们可以认为这个类的职责就是维护用户信息。

那如果类变成了这个样子呢?

单一指责反例2

我们在用户信息业务类中增加了登录和更新积分的方法。那么这两个方法还在类的职责范围内吗?我认为是不在的。因为用户积分的更新通常是比较复杂的业务,比如用户总积分的更新、积分的有效期设置、用户可用积分的计算,还有用户积分使用,通常需要根据一定的规则扣除用户有效期内的积分。如果将这个业务放到用户信息类中吗,那么这个类将变得非常的臃肿。而拆分开的话会很清晰。

用户登录也是如此,显然用户登录其实跟用户信息的维护的核心业务并没有很大的关系。所以此处更应该拆分。而且随着其他登录方式的支持,如支持移动端的登录、第三方OAuth2的登录等。

所以我认为变更后的UserInfoService应该这样来设计

单一指责拆分业务-CQRS

这样的设计使我们的代码清晰了很多,我们其实是将类按照业务划分为了3个类

  • 用户基本信息的查询和维护
  • 用户积分的管理
  • 用户登录

用户积分的赠送规则修改的话只需要关注UserPointService,登录业务的修改只需要关注UserLoginService。而不是每个跟用户有关的业务修改都要修改UserInfoService

实例2:创建订单

来看下面创建订单的一个例子。

单一指责订单实例-反例

类中包含的方法说明:

  • checkStock:校验库存。
  • checkCoupon:校验优惠券是否满足使用条件,或所选优惠券是否已使用。
  • calcPostage:计算运费
  • calcTax:计算税费(跨境订单需要计算税费)
  • subtractStock:减库存
  • generateSnapshot:生成交易快照
  • saveOrderInfo:保存订单信息

这个类只负责了一件事,就是创建订单。但是它是单一职责的吗?显然不太像是!这个类对外只有一个职责,但是在内部有很多职责,而且各个职责相互独立。所以这种设计其实是不满足SRP的。

那么应该如何重构这个类让它满足单一职责呢?

单一指责订单实例-拆分指责

通过这样拆分,简化了OrderCreateServiceImpl的职责。而将职责分给了其他的类。因为订单中的每项业务处理都是很复杂的,比如说运费的计算,需要判断订单中是否有包邮商品,如果有则整个订单运费为0,如果没有则需要判断订单中的所有商品是否满足包邮条件,如果不满足还要根据商品的重量来计算运费。我们这样设计的话,如果运费规则发生变化,那么我们只需要修改OrderPostageHelperImpl就可以了,而不用动OrderCreateServiceImpl

印证单一职责作用的第3点。

总结
职责的边界是很难划定的,不能划分的太细。也不能划分的太粗略。粒度很难把握。所以我们要根据业务的复杂度来确定边界,单一职责的目的是为了降低程序复杂度,是代码更容易阅读和维护,并且实现高内聚低耦合。只要我们达到目的,我认识单一职责原则应用的就是好的。不用单一职责和滥用单一职责都会给我们的项目造成很大的麻烦。

我们的类是如何不满足单一职责的

我们在程序开发初期,我们的类一般都是满足SRP的(或者说大多类都满足)。随着项目维护的时间越来越长,结果发现满足SRP的类越来越少。结果导致项目中的类代码越来越多,甚至有的类能有几千行代码,数十个方法。

出现这种问题的原因主要有以下几点:

  • 开发人员无视单一职责原则
  • 开发人员的惰性
  • 项目经理没有代码质量的要求,或者有要求但是没有检查。

很多开发人员根本没有去了解过设计原则(我身边有大量的例子),他们认为只要是针对某个实体(Entity)业务的代码就应该在一个类中。比如用户信息(UserInfo),只要是跟用户相关的业务,无论积分变动还是登录甚至是购物车业务都应该在用户信息类中。这种程序员是很可怕的,堪称模式杀手(Pattern Killer),无视任何原则与模式,代码随意复制粘贴,如果你的项目组中有这样的员工,那你真的很不幸。

我相信大部分程序员是知道单一职责的,只不过是因为需求变动导致新加方法或修改原来的方法,不想新创建类也不想抽取代码组成新的类(因为还要想类名,还要写接口等等原因),直接在相关的业务类上增加方法,导致类的职责越来越多。

出现以上两种情况最主要的原因,我认为是项目经理或者组长没有对代码质量的要求和检查(有要求没检查等于没有要求),一直放纵开发人员这种无视设计原则的状态导致的。

如何缓解此类问题?提出几点建议

  1. 要求每个类中的代码行数,比如每个类最多300行代码。因为类职责的增加必然带来的是代码行数的增加。
  2. 限制方法的代码行数和复杂度(条件、循环的深度和数量)。限制方法的代码行数可以确保方法的复杂度不会很高,可以督促开发人员对方法内的业务进行拆分。
  3. 限制每个类中方法的数量,比如15个。

指标比开发人员的自觉要管用的多。(我不是针对某些开发人员,我是针对所有开发人员。其实是开个玩笑!)但是也要综合考虑开发人员的构成以及项目成本等因素。

Break Single Responsibility Principle(反SRP)

这篇文章上举了一些反SRP的例子,感觉说的有点儿牵强。有兴趣的同学可以看看。

1、单一职责原则(Single Responsibility Principle)

http://jaune162.blog/design-pattern/design-principle/srp.html

作者

大扑棱蛾子(jaune162@126.com)

发布于

2024-02-01

更新于

2024-02-22

许可协议

评论