简化你的代码:分离业务逻辑与技术细节

开始之前,我们来看看下面这一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function couponCron() {
$now = Carbon::now()->toDateTimeString();

// 到开始时间了,未开始的优惠券活动更改为开始状态
app(Coupon::class)
->where('coupon_status', 1)
->where('release_start_time', '<=', $now)
->update(['coupon_status' => 2]);

// 过期优惠券活动
$expireCouponIds = app(Coupon::class)
->whereIn('coupon_status', [2, 3])
->where('release_end_time', '<=', $now)
->pluck('id');

if ($expireCouponIds) {
// 过期优惠券活动状态改为失效
app(Coupon::class)
->whereIn('id', $expireCouponIds)
->update(['coupon_status' => 4]);
}
}

这段代码是一个定时任务,用于处理优惠券活动的状态变更:到了优惠券开始时间,将优惠券状态更改为活动中状态;到了优惠券结束时间,将优惠券状态更改为失效状态。

没有想到非常好的命名,就这样吧。

这段代码有什么问题?

像以上这个规模的代码,并没有什么问题,因为足够简单,我们看几秒注释就能知道这段代码的作用。 但是当这个函数几百行的时候,我们估计会比较头疼了,比如下面这种六七百行的:

单单比较长度可能看不出什么猫腻,我们再看看下面的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public function couponCron()
{
$this->startCoupons();
$this->invalidExpiredCoupons();
}

private function startCoupons(): void
{
$now = Carbon::now()->toDateTimeString();

app(Coupon::class)
->where('coupon_status', 1)
->where('release_start_time', '<=', $now)
->update(['coupon_status' => 2]);
}

private function invalidExpiredCoupons(): void
{
$now = Carbon::now()->toDateTimeString();

$expireCouponIds = app(Coupon::class)
->whereIn('coupon_status', [2, 3])
->where('release_end_time', '<=', $now)
->pluck('id');

if ($expireCouponIds) {
app(Coupon::class)
->whereIn('id', $expireCouponIds)
->update(['coupon_status' => 4]);
}
}

跟前一个例子不同之处在于:

  1. 将代码拆分成了两个函数,每个函数只做一件事。
  2. 原来的那个函数里面只需要调用两个函数即可。

只是对数据库操作做了一下封装,是这样吗?

从表面来看,是的,只是对数据库操作做了一下封装。但是从更深层次来看,也没有那么简单,我们将业务逻辑和技术细节分离开了:startCoupons 以及 invalidExpiredCoupons 是直接告诉读者业务是什么,而 startCoupons 函数的实现则告诉读者如何实现这个业务。 在我们只需要了解大概业务逻辑的情况下,无疑第二种写法能够更快地让我们理解代码,只有当需要去对其中某些业务逻辑做变更的时候,我们才需要去看具体的实现。 函数过长的一个原因往往就是,代码全都是描述如何实现业务逻辑的技术细节,而不是描述业务逻辑本身。

另外一种说法是:抽象层次不一样的代码混在一起,可能有一部分代码是描述业务逻辑的,然后一部分代码是描述实现业务逻辑的细节的,描述实现细节的代码我们需要看注释才能知道背后的业务逻辑。

在《重构》中,对于长方法,有这么一句话:如果你觉得需要对方法中的某些内容进行注释,你应该把这段代码放在一个新的方法中。如果需要解释,即使是一行也可以而且应该拆分成一个独立的方法。如果方法具有描述性名称,则无需查看代码即可了解它的作用。

《重构》中没有说到的

其实对于上面的这种问题,可能很多人也有意识到,要将函数写短一些,但是这种意识可能并不能很好地指导我们的实践:每个人标准不一样,比如每个人对于方法的 “短” 的定义不一样,有的人觉得 100 行就很长了,有的人觉得 200 行也没问题,当然实际中也不可能只从方法长短来判断一个方法写得好不好。 那什么东西应该被用来指导我们去在适当的时候将方法写短一点呢?我觉得可以从以下几个角度重新审视一下开篇的那一段代码,然后我们就很容易理解了:

  1. 抽象层次:这个方法是描述业务逻辑的,还是描述实现业务逻辑的细节的?如果我们的方法是一个 API 或者某个逻辑实现的主入口,那我们应该将这个方法写得尽可能短,然后将业务逻辑的实现细节放到其他方法中。
  2. 代码可读性:方法太长之后,我们可能看几行代码就得想想这几行代码到底做了什么操作,对后面的代码有什么影响。这样导致的一个问题是,我们的思考过程不会连贯,而是断断续续的,这样会导致我们的思维负担加重。
  3. 可维护性:需要花费更多的时间去理解这个方法,然后才能修改它。另外一个问题是,可能其中某些技术的细节是可以复用的,在其他地方如果也有类似的细节,我们可能会重复写一遍。

什么是业务逻辑?什么是技术细节?

举个简单的例子,有个用户注册的接口的逻辑是:

  1. 通过账号密码注册,然后发送邮件给用户。
  2. 为了防止黑客批量注册,注册的时候需要填写一个验证码。在注册之前,需要先验证验证码是否正确。

在这个例子,主要的业务逻辑有:

  1. 验证:账号密码验证码是否为空、验证码是否正确、用户是否已经存在。
  2. 注册:在数据库中创建一条用户记录。
  3. 发送邮件:通过某个邮件组件发送邮件给用户,告知用户注册成功。

上面提到的这三点,冒号前面的就是业务逻辑,冒号后面的就是技术细节。对于整体的业务逻辑,我们的伪代码可以写成这样:

1
2
3
4
5
6
7
8
9
public function register($account, $password, $captcha)
{
// 验证
$this->validate($account, $password, $captcha);
// 注册
$this->createUser($account, $password);
// 发送邮件
$this->sendEmail($account);
}

而技术细节部分,我们只需要在各自的方法中实现即可。这样的好处是,自己或者别人再看这段代码的时候,看到的首先都是业务逻辑,而不是一上来就陷入到技术细节中无法自拔。

分离开业务逻辑与技术细节的好处

其实写代码跟写作有点类似,因为二者都是有点 “写作” 的味道的,当然写作的创作性更强;另外就是,写代码跟写作最终都会让被人阅读,所以我们写代码的时候,也要考虑到别人阅读的问题。

对受众(包括读者、听众、观众或学员)来说,最容易理解的顺序是:先了解主要的、抽象的思想,然后了解次要的、为主要思想提供支持的思想。因为主要思想总是从次要思想中概括总结得出,文章中所有思想的理想结构必定是一个金字塔结构-由一个总的思想统领多组思想 - 《金字塔原理》

当我们将业务逻辑与技术分离之后,“业务逻辑” 就等于是我们的 “主要思想”,而 “技术细节” 就等于是 “次要思想”。这样就能让读者很容易就能理解我们的代码。

我们该如何做好分离?

业务逻辑与技术细节耦合的最常见的一种场景是,在我们实现某个功能的时候,写到哪一步就做哪一步业务逻辑所需要的 CRUD 操作,当然可能不同语言、框架底下写出来的代码会有所不同,但是我们接触的大多数都是这样的代码。

当然并不是说这是错的,这是很正常的,实现功能的代码一定是长这个样子的,很少纯粹只有业务逻辑的代码,纯粹的技术的代码倒是有很多(比如 RPC 通信的底层封装)。我们要实现某些功能,肯定会跟各种数据打交道,又或者跟 RPC 之类的东西打交道。 而且完全分离业务逻辑与技术细节是不可能的,因为完全分离也会带来其他的问题,很多简单的场景下还不如写到一块去直观。

只能说,尽量保证方法短小,我们有很多时候可以选择将方法中的一些处理技术细节的代码抽取出去(Extract Method),比如在其中混杂了一个复杂的查询,写了十几行了,这个时候拆分出去或许会更好,让原方法只保留核心业务逻辑的描述。 如果原来方法本身就很短,并且看多两眼就能看懂在做什么的话,拆分了反而会让代码变得更加复杂,这个时候就没必要拆分了。当然,代码的好与坏不应当用这种 “含混的代码美学” 来判断,因为这会带来一个困难是,每个人对于代码理解不一样,所以做出的判断不一样。 但是,这种分歧是没有办法避免的,可能有个办法是,每一个人负责某一功能模块,不要大家改同一块代码,但这个人离职之后,这个人的代码其他人还是得去维护;又或者,另外一个办法是,定一个标准或者说规范,然后大家共同遵守,比如方法不能超过多少行。

总结

可能我们也知道方法长了得拆分一下,因为我们都知道,代码太长看起来太费劲了,但是可能我们并不是很清楚背后的一些原因,导致我们在实践的时候做不到很好地去拆分。 本文从一个新的角度探讨了如何通过分离业务逻辑与技术细节来简化代码,提高可读性和可维护性。合理地对业务逻辑和技术细节进行分离,可以让读者在阅读代码的时候,更容易理解代码背后的业务,而不是被技术细节所迷惑。