“重构,一言以蔽之,就是在不改变外部行为的前提下,有条不紊地改善代码”。

技术升级与业务升级

《重构——改善既有代码的设计》一书已经很充分的介绍了如何做重构。如果我们只需要对一小段流程、一小部分代码做重构,这本书已经提供了非常实用的工具。不过,如果我们要对整个系统做一个全面的升级改造,书中的技巧就有些“一叶障目不见泰山”了。

技术升级

而且,技术升级其实并不难。

首先,大多数情况下,技术方案都是比较通用的:你也用SpringCloud,我也用SpringCloud,我俩的方案即使不是一模一样,相去也不过毫厘之间。

其次,技术升级一般会采用开发人员比较了解和掌握的技术,这样设计、实施起来会比较得心应手。因此,这类升级这里不多说。

业务升级

但是业务升级却恰恰相反。

首先,与通用的技术方案不同,业务逻辑就如孙悟空一般,同样的业务可以变化出七十二种不同的方案来。例如,同是账务系统的记账业务,就可能有单边账、双边账、会计科目账等不同的记录方法,账务系统A的设计方案与账务系统B的设计方案也许就是水火不容的。

其次,开发人员对业务的了解和掌握程度,既不像对技术那样深入,也不像产品或业务人员那样熟悉。因此,由开发人员来升级业务系统,颇有点强人所难了。

尽管更加困难,业务升级有时比技术升级更加迫在眉睫。

很多业务系统从立项伊始就伴随着业务的“野蛮生长”,因而不得不采取疯狂堆代码、先上线再说这样饮鸩止渴的策略。遵循这种策略开发出的代码,很快就会陷入高度冗余、高度耦合的泥潭中,并由此导致业务逻辑不清晰、改一个需求动一万行代码、天天加班需求还是搞不完、好不容易上线了却bug不断等种种问题。

雪上加霜的是,由于陷入其中无法自拔,开发人员既没有精力去提高自身技术水平,也很难忙里偷闲来对系统做技术升级。

何况,应对这些问题时,技术升级并不是对症的良方:由于对整个系统的理解上一叶障目不见泰山、或者改造时牵一发而动全身,技术升级往往只能得到局部最优解而非全局最优解。就更不要说技术升级有时还要依赖业务升级的成果了。

不知道算幸运还是算不幸,我做过好几个业务系统的升级改造。从这些工作中,我总结了一些业务系统全面升级改造的思路。

建立领域模型

大多数业务系统在建立之初都没有一个完整的模型,业务都是靠着一个一个的业务流程,甚至一个一个的功能点拼凑起来的。可以说这是业务系统的一个原罪。

在业务开展和业务系统建立之初,用这种模式来开发系统无可厚非。但是在业务成熟之后,业务系统进行升级改造的时候,仍然沿用这种模式,可以说是一种治标不治本的行为。只要沿用这种模式,前面提到的业务系统的种种问题,一定会很快重现,甚至变本加厉。

要跳出这样的模式,我们就必须建立一个领域模型,并且必须由这个领域模型来全局地、统筹地管理业务,而不是一点、一线的拼凑业务。

在设计出一个合理的领域模型之后,才有可能梳理出简洁的、清晰的、完整的业务逻辑,才有可能写出高内聚低耦合的代码,才有可能降低业务开发的工作量和bug率,才有可能放开手脚去做技术改造、让系统获得技术升级的同时,让开发人员提高开发水平。

大话说了这么多,怎样才能建立领域模型呢?

首先我们来想想,领域模型应该是什么样子的?从我的经验和理解来看,一个领域模型应该包含两套东西:一套数据结构,一套业务抽象。说到底,程序等于算法加数据结构嘛。

业务数据结构设计

一般来说,数据结构主要关注一对一、一对多、多对一、多对多等几种数据关系,以及类型、长度、主键、唯一键、外键等数据约束。熟悉数据库表设计的话,对上面这些一定驾轻就熟了。

数据结构设计 != 数据库表设计 != 接口字段设计

但是,只有在业务非常简单的情况下,领域模型的数据结构设计才能与数据库表设计画上等号。在复杂的业务领域中,我们更应关注的是在内存中的数据结构。数据库表仅仅是业务数据在持久化时的一套映射关系,并不是数据本身。

举一个简单的例子。很多系统都会需要一个配置服务,即给定一个配置关键字,返回一个配置值。这个值有可能是一个数字,也可能是一个字符串、布尔值,甚至是一个复杂对象。这个值的实际类型就是我们在内存中使用的数据结构。

但是对数据库设计来说,我们没有必要完全按照这个数据结构来设计库表,而可以借助NoSQL的方式、用Json或者Bson的格式将配置值存储在数据库中。这时候数据库表的结构与实际使用的数据结构之间就出现了差异,这种差异就是前面所说的业务数据在持久化时的映射关系。

再举一个复杂点的例子:如果要设计一个在线Excel系统,当一个公式涉及到多个单元格的时候,怎样能把这些单元格相互关联起来呢?链表是一种很容易想到的数据结构。但是链表结构要如何存储到数据库中呢?比较简单明了的方式是只在数据库中保存公式,从数据库读取数据并解析为单元格的时候,再根据公式中解析出链表关系。

在这个例子中,我们使用的数据结构是列表,而存储的数据结构却只是一个字符串。二者借助公式解析功能来完成映射。

在实际工作中,不只是数据库表结构与业务数据结构之间存在差异和映射,接口出入参数与业务数据结构之间往往也有差异和映射。这里就不举例子了。

总的来说,我们在设计业务系统使用的数据结构时,虽然可以以数据库表或接口输入输出为基准,但不应该被他们束缚住思路。归根结底,业务数据结构才是业务系统的核心结构,数据库表或输入输出应当围绕业务数据结构来设计,而不是业务数据结构围绕数据库和输入输出来设计。

不与数据库表结构挂钩的话,业务数据结构应该如何设计呢?

有一个比较简单的技巧,即现实业务的中数据是什么样的,我们的业务数据结构就设计成什么样。换句话说就是临摹。实际业务中的数据关系是一对多的,我们就用一对多的结构来表达,而不要为了图方便把多条数据压缩成一个字符串来表示。实际业务中的数据类型是整数,我们就不要把它设计为字符串。

这样做有什么好处呢?

首先,业务逻辑是建立在业务数据上的。如果我们在操作业务数据时,需要引入额外的逻辑,等于是在为自己的业务逻辑增加额外的复杂度。

例如还款计划表的每一期都有期初金额和期末金额这两个数据,并且当期的期初金额等于上一期期末金额。

如果我们在设计它的业务数据结构时,每一期都只保存期初数据,那么当我们需要去获取期末数据时,就不得不查出下一期的期初数据。只保存期末数据也有同样的问题。

由于这种查询操作非常多,这样的额外操作也非常多,其中的性能浪费就很可观了。

但如果我们为每一期都保留期初和期末两笔数据,就可以生成还款计划表时计算一次,此后都直接查询就可以了。

其次,附加在业务数据结构上的逻辑,往往只能匹配某些特定的业务需求。当业务需求发生变更时,这些逻辑往往就不能满足新的要求了。

例如,我们曾经用一个数组来把一对多的关系压缩为一对一的关系。但是这样的压缩只能够满足一级的一对多,如果业务逻辑演变成多个层级的数据关联,如1对n对m,这个设计就满足不了要求了。

总的来说,数据结构的设计应当直接“临摹”业务数据结构。数据库、接口的设计应服务于数据结构,而非相反。

业务抽象模型设计

业务抽象的设计有点象做阅读理解:你需要钻进去,然后再跳出来。

所谓钻进去,是指理解业务最核心的本质,并且对业务细节有充分的掌握。所谓跳出来则是从繁琐的细节中抽离出来,站在一定的高度上对业务进行抽象描述和设计。

其中最简单的就是掌握业务细节这一步骤。在这项工作上只要能肯花费足够的时间和精力,就能够有非常大的收获。

理解业务核心本质和对业务进行抽象设计有异曲同工之处:我们都需要抹去一些细节上的差异,站在更全面的、更高层次的角度上去分析贯穿于不同细节之中的业务本质。

还用在线Excel系统举例子。Excel都需要支持哪些功能呢?我们输入一个文本,系统要能支持按不同格式展示出来。单元格要支持输入公式,输入公式后,要能计算出它的结果。某一个单元格改动后,所有与之相关的公式都要能自动重新计算。等等等等。

把这些纷繁复杂的流程和功能分析过之后,我们可以发现Excel的核心逻辑是三项:展示、计算和传播。据此创建出三个模型,就可以实现我们所需的大部分功能了。

又比如我们要做一个还款系统,需要支持正常还款、逾期还款、提前还款、部分提前还款等等若干种不同的还款模式。

虽然还款模式很多,但是他们的核心逻辑都是一样的:计算应还金额、记录收付款的账务、更新还款计划表。根据这三个核心逻辑,我们就可以创建三个模型,从而为上述不同的还款模式提供。

在设计业务抽象时,最重要、也许也是最困难的一点在于:要把业务抽象定义在合适的层次上。在这个合适的层次上,我们的业务抽象既不会过于抽象、也不会过于具体。

过于抽象的设计,就像Java函数式接口Function一样,虽然包罗万象,但是大而无当。这种设计会失去业务逻辑中所包含的约束、隐喻等,因而无法为后续的开发提供实质性的指导。

另一方面,过于具体的设计则很容易陷入纷繁复杂的细节之中,即使后期努力优化、重构,也很容易因为一叶障目而只能求得局部最优解,甚至连最优解都算不上、而只是补丁摞补丁。

其它方面设计

数据结构和业务抽象可以清楚地勾勒出一个系统有哪些业务功能,以及它要怎样实现这些功能。

但是,除了业务功能需求之外,我们还应当关注一些其它需求,如扩展性、可用性、幂等性、请求响应时间、事务吞吐量、数据库锁性能、以及可监控、可运营等等。

可扩展性

从个人经验来看,业务系统的设计中,应当重点关注扩展性的设计。因为业务系统所面对的诸多问题中,最司空见惯、也最令人生厌的就是需求变化。上午时的业务流程还是“A-B-C”,开一个会之后就变成了“A-B-D”,再写几行代码又变成了“A-B-D-C”,随着测试介入,最后变成了“A-B-C-D”……类似的场景相信做业务系统的各位应该是见怪不怪了吧。

如果在项目前期不能设计出一个高度可扩展的项目框架,那么当业务需求一变再变的时候,加班赶工、熬夜改bug等后续问题就会接踵而至了。一个扩展性高的系统,应该要做到当需求在同一维度上变化时,只改数据、不改代码就能支持;引入新的维度时,只需简单扩展,不必伤筋动骨就能支持。

可监控性

可监控和可运营是很容易被忽视的两个设计要点。

有一句话大家应该都听过:“用户输入是魔鬼”,如果我们对用户的操作、输入和输出没有充分的监控(有时甚至要监控到用户的一条完整操作链),有时我们完全无法想象用户会用什么样的方式来操作我们的系统、会提交什么样的数据,因而也无法想象我们遇到的bug和问题究竟是怎样产生的。

我以前做过一个web系统,需要用户分两步提交一些数据,每个步骤对应一个web页面。系统上线后我们发现,从第二个页面提交过来的数据中,有很多都缺少第一页的数据。这是为什么呢?

我们绞尽脑汁、查了大量的日志和数据,最后发现有些用户在填写完第一个页面后,会过好几天才打开第二页进行填写。然而几天过去了,这时提交的第二页数据就无法和第一页的数据关联上了,自然也就没有了那部分数据。推测是用户在填完第一个页面后,把第二页放进了收藏夹,后来直接从收藏夹进行访问。

我们是怎么发现这个问题的呢?系统日志中记录下了用户请求系统的URL,我们经过分析后发现,这类问题数据都只有第二个页面的请求记录,而没有找到相关的第一个页面的请求记录。如果当时我们没有这个日志监控,想要发现这个问题的原因,简直天方夜谭。

可运营性

监控的重要性已经被很多人认识到,并且在设计中会重点考虑。但是可运营性就真的很少被提及了。

我所谓的“可运营性”,是指当需要对线上数据做修改时,可以通过系统中预定义的一些业务流程来执行操作,而非原始的修改数据库。

与手工写SQL语句修改数据库相比,通过系统来执行业务操作有很多好处:保持业务数据一致性,不会漏改/错改某些数据;简单方便;可以通过系统权限控制来限制和追踪操作人及操作数据等等。

例如,做过账务系统的朋友一定都遇到过改账(例如回滚或者对冲某笔账目)的需求吧。在我们的账务系统中,一个业务操作(例如放款、还款、垫付等)可能对应到上百个账户的余额、流水等数据,如果要手工改账,简直是impossible mission。

于是我们设计了两个特殊的记账业务,分别用于指定对冲某一条账目和对冲某一笔业务引发的所有账目。这两个功能上线之后,相关的开发人员就从查账、写SQL的工作中解脱了出来,从而有更多时间和精力去做个人的技术提升和系统的技术优化工作了。

其它

其它的可用性、幂等性、高性能等方面,相关的文章、讨论已经非常多了,这里不多赘述。