抽象与责任分离

之前有写过一篇文章,介绍类基多态与模式匹配的区别与适用场景的不同。但是其中并没有具体的例子,不免有些空泛。一转眼两年多了,这次找了一个具体的例子,一步步分析各种不同的方案,或许更好理解些。

问题

在银行交易系统中,有很多不同的交易类型,每种交易类型的数据结构都有所不同。同时,银行每天会把所有交易向用户报告,报告的格式也有很多种。需要一个模块,处理所有交易数据,并生成各种不同格式的报告。

交易类型比如有:

  • Deposit
  • Credit Transfer
  • Direct Debit

报告文件类型比如有:

  • Csv
  • Text
  • SWIFT

方案

在不做任何设计和类的划分的情况下,无非就是把所有的逻辑都放一起,于是我们就得出了这样的一个方案:

全都放在一个叫File Generator的类里来处理。于是,无论是有新的交易类型,还是有新的文件格式,这个类都是需要改的。这显然违反了单一责任原则。

这个问题很好解决的嘛,只要让每个Generator,只处理特定的某一种具体情况,然后每次有新的情况,只需要新加一个Generator就好了,高内聚,低耦合,可扩展,而且写好的Generator代码就再也不用改了。就像这样:

其实呢,本质上,这仅仅只是把原来的大的FileGenerator分成了一个个小的类(或函数)而已。换个思路想一下,当Deposit交易的数据结构有变化时,你需要动几个类?在这个设计下,是不是需要动的类的数量反而越来越多了?(有时,改的类的数量多并不是问题,但是这里,假定是。)

嫌类多啊?那我们分析一下哪种变更比较频繁然后有针对性地优化嘛。

如果时常加新的输出文件类型,那可以针对加文件优化:

于是每次有新文件类型,只要加个新Generator类就好。如果时常需要加新的交易类型,那又可以针对交易类型优化:

于是每次有新交易类型,也只要加个新的Processor类就好了。

最后还要再加一句:“就看业务上能不能判断出来,到底是交易类型加得多,还是报告文件类型加得多了。我们技术上都好办。”成功把一个纯技术问题成功甩给业务方。然后尽看业务方一脸蒙逼。

真的就好了吗?技术就是这么解决问题的吗?

引入抽象层隔离依赖

以上所有的方案,无非是同样的一坨坨代码,到底是分成几个函数,几个类的差别而已,本质上都是一样的。

上面所提的各个方案中,所存在的根本问题是:没有抽象,也就没有正确使用多态的能力。

Generator所面对的,一边儿是输入,是一个具体的数据类,另一边儿输出,也都是一个个具体的数据类。编程原则上常讲,要面向接口编程,而不要面向具体细节编程1。上面的Generator,可说是反面典型。

当一个模块(可以是一个类,也可以是一个函数,也可以是一组类),要实现一个功能的时候,应该基于一层抽象层来构建自己的逻辑,这样才能保证自己的逻辑的稳定性,而不会被具体实现的变化牵连产生衍生问题。

以上面的File Generator为例,用于生成文件所要依赖的东西(即数据),应该是一个抽象的概念,而不是任何具体的数据结构。

比如,如果是要生成交易流水,那么,我们可以为生成交易流水这个函数,先定义一个抽象的,不依赖任何具体业务的概念,说,我要生成流水,就需要这些数据,别的都不用了。然后我们可以用一个专门的类来表达这个概念,比如叫Statement,那么,整个设计可以变成这样:

同时,原来的File Generator,其实分成了两个部分,一个部分负责把具体的场景化的业务数据,转成统一的抽象的数据;另一个部分负责将这个Statement,再根据具体的情况,生成不同格式的文件。由于这个Statement类,就是File Generator负责定义的,File Generator的逻辑也是基于这个抽象的概念来写的,其逻辑也就是稳固的。同时,Generator的责任也更单纯。

上图中的Statement Extractor独立出来的好处是可以让具体的数据类,如Deposit,Transfer等,对其具体使用场景(用于生成Statement文件)无感而引入的。但是如果在特定上下文中,这些具体的数据类,可能会具体场景有感知,就没有必要总是把数据转换的部分独立出来。可以合并到一个基类中提供实现,如下所示:

另外,上图中的File Generator,也可以直接引用Transaction基类。但是不应该直接引用任何具体的子类。

应用

上面的示例,仅仅用来解释,在上述问题中,如何通过引入抽象的数据实体,分解责任并构建更加稳固的代码逻辑。但是并表示:

  • 同类问题只有这一种解决方法。
  • 只要有这类问题,就一定要引入抽象层才是更好的。

每一层都是额外的复杂度,只有当被解决的问题的复杂度大于解决方案复杂度增加带来的新的认识负荷时,才值得增加这额外的一层。

不要走极端

见过不少代码,所有的类,都实现一个与类名一致的接口,从上层的Controller到底层的数据库皆然,问其缘故,说是要“面向接口编程”,所以这样做的。我就在想,这也不知是被哪个没耐心的大佬坑了,同时脑子里不由自主地就浮现出这些对话的画面。

没耐心大佬:“你这个代码能不能加个接口?面向抽象编程懂不懂?”

小白:“好的,您看是不是这里?”

没耐心大佬:“对。”

小白:“好的,那这里呢?要不要加?”

没耐心大佬:“也要。”

小白:“好的,那还有这里呢?加不加呢?”

没耐心大佬:“……,这样吧,你全加吧。全加也比全不加好。”

发表回复