业务中台探索和实践:软件的根本问题
大多数业务软件都可以叫做数据系统。他们基本结构如下图:
大多数业务软件系统都符合如图的结构和公式 y = f(x):
对于大多数企业级系统而言,多数业务都是可重现的。故若流水表定义为 y 撇(y 的一个可逆变换), 则必有函数 fy 存在使得 y = fy(y 撇)。我们认为 y 和 y 撇在信息容量上是等价的(或者说在集合上是等价的)。
很显然,流水表包含了所有的业务要素。简单证明如下:
如果业务请求 x,则 x 包含所有业务要素。且业务请求 x 数量是无限集合 X。软件 f 和配置表皆是有限集。若业务系统有持久化,业务 可重现 。则业务要素必然只能存储于流水表,且流水表必然包含所有业务要素。
对于任何一个业务系统,我们得到结论或者推论:
以上四个推论对数据系统的设计工作是有极大帮助的,它们将改变我们传统的设计模式四大要求和业务软件的矛盾。
我们知道我们不可能让业务去学技术,而只能让技术去学业务。但是学习成本非常高,时间消耗非常大,学习边界和效果不好衡量。通过推论 1 和推论 2,使得技术人员对业务的学习和掌握变的更有目的和边界。
我们都说系统是演变过来的,而不是设计出来的。这句话一无是处,又极具欺骗性——任何事情都有过程,好的系统有,烂的系统也有,好的系统的过程自然不能否定这句话,可是我没见过哪个烂系统因为这句话变好过的。这句话在现实中有两种场景会出现:
它的唯一有益的是逃避了设计工作——设计对业务开发没什么实质帮助(这是普遍情况)———为业务开发争取了足够的时间,加快业务响应效率。但是在有了可行的设计方法,能够解决设计和业务新系统矛盾的情况下。正确的做法既不应该逃避事前设计的,也不应该逃避时候责任。推论 123 是帮助我们事前快速设计系统的方法,推论 4 是帮助整体架构演进的基本依据。
通过以上四个推论(业务的数据要素模型),结合业务流程图,基本上可以在短短几分钟到几个小时内完成需求的设计,并且这种设计是过去和现在情况下的最优解。将使设计工作变的可落地,客观。通过以上四个推论,可以在一天或者几天之内梳理完一个遗留系统的全部业务,给出最优设计方案,当前问题和改进办法。不单单是基于用户 UI 或者遗留系统,对新建系统和需求呢?它同样是有效的,我们只需要多做几个点:
我们在过去较长时间在大量系统中实践过和推广过这些办法,效果是非常惊人的。对于遗留系统基本上能在几天和一周之内分析清楚并给出改造意见,对于新系统,领域设计和开发速度、后续业务支持能力、响应速度和风险效果都非常好。开发人员也反馈成长很快。
同时要注意的是,这种方法是针对数据系统的特殊结构取巧的一个解法。并不一定普适于非数据系统。譬如,对于中间件系统,CAD 类系统,或者风控系统等,它依赖于数据系统结构,依赖于流水表,依赖于可重现。
通过这些办法,我们利用数据系统的一些特征,去掉了传统设计方法中过于模糊、不可操作和落地,成本高昂的部分。让业务系统真的变得可设计,让设计真正变得可落地。
大多数业务系统都属于数据系统,都可以按照上文中的模型去设计和处理。但是我们必须提醒:虽然我们上文一直讨论的是流水系统模型, 但是一个流水表,就是一个系统么 ? 流水数据模型某种层面只是帮助我们抓住业务要素 的一个办法,并不是划分系统的依据。
在抽象层面,所有的流水表都可以抽象为一个表。通过 type 来区分就可以了。但是 type 和 type 之间的关系数量,以及业务系统的代码复杂度会急剧上升。也可以一个流水表一个系统,这个系统的复杂度会非常低,但因为流水表之间有耦合内聚性,这导致这个系统和其他系统有较强的依赖关系,会导致整体的复杂度上升。这方法本质是把对象依赖转变为系统依赖,开发效率会极大降低,协作成本,通讯成本会极大提升。甚至有很多极端的一个 API 一个系统的,接近于走火入魔了。提到以上两个办法看起来是啰啰嗦嗦,错误很低级,似乎正常人都不会犯,似乎提这些是非常没有必要。可是近年来随着工匠精神的崛起,过于强调技术,为技术而技术,导致这些看起来非常低级的错误都大量涌现,甚至广为传播,大加宣扬。
还经常有技术人员有类似的想法——我们可以发明一个业务系统,能解决所有的业务问题。我们可以发明一个系统,能解决所有的业务问题。或者我们应该拆分的更细一点,更微服务一点,更 SOA 一点,对象单一职责嘛,越简单越好。要我说,一个系统能解决所有的业务这种东西已经有了,但是大家都不满意,比如 Object,比如 Interface,它什么都是,但你没满意。
你不能做出让所有人都满意都系统,因为存在“复杂度”这个因素。你越抽象,客户化要做的就越多,差异化要做的就越多,你那里少做了,这里就必须补回来,因为 业务目标的业务复杂度是恒定 的,不可减少的。设计上少做了,抽象了,业务上就多做了,繁琐了。细节上简单,少做了,简单了,总体上就复杂了,多做了。这是一个从系统设计到对象级编码都会大量犯的错——没有意识到软件设计和复杂度的关系。
为所有的复杂度要素划分最合理的领域,寻求最优化的展现形式,寻求最合理的分工是系统划分的目标。传统上,内聚性较高的流水表之间封闭为一个系统是一种很好的办法,但并非最优办法。这也是很多创业公司最实际的做法——就一个系统,无论用户系统,还是业务系统,都在里面。因为这几个领域虽然彼此耦合性不高,但是也并不足够复杂。这种形式成本最低,也适合初创期精简的团队。即使在很多大型公司,也常常见到很多彼此领域无关的几个业务做在一个系统中,比如将省份数据、图片管理等统一封装到一个 common 项目。这几个领域毫无任何关联关系,这是形式成本最低和分工决定的。
我看到有大量公司,有大量程序员,盲目效法别人的成功做法,或者知名大公司的做法,盲目跟风业界潮流,没有正确的软件架构观,不能思辨的吸收,导致系统腐化,僵化的并不鲜见。毛主席说这是教条主义。
在我看来, 在不违反耦合性和内聚性的前提下,总是选择成本最低,总复杂度最低的方案。对不同的企业,虽然有部分业务是相同的,但是并没有一层不变的系统划分标准 ,未必需要按照其他企业的标准划分,还是要具体情况具体分析。多数情况为了省事,可以拷贝,但是这不意味着是最合理的。
以我多年的系统重构经验,大多数看起来非常复杂的业务系统经过抽象以后其实都非常简单,完全没有进行细粒度拆分的必要性。合并是一个更合理的选择。合并比拆分好,合并会导致总复杂度降低,运行成本降低。
但是合并有一个上限。一个系统的总体复杂度不能超过 owner 它的团队的可控制复杂度的能力上限,我认为还是要保持一定 buffer 比较好——也许 1/2,也许 2/3。
简单总结一下:
软件是由模块组成的,模块应该是什么样子的?依模块和软件的关系,所有可能的形态有 6 种:
这 6 种形态的特点如下:
可表达复杂度 | 开发协作成本 | 性能 / 消耗 | 灵活性 | |
---|---|---|---|---|
模式 1: 代码行 | 最低 | 最低 | 低 | 高 |
模式 2:函数 | 较低 | 较低 | 低 | 高 |
模式 3: 类 / 接口 | 低 | 低 | 较低 | 高 |
模式 4: 本地模块 | 高 | 高 | 较低 | 高 |
模式 5: SOA | 最高 | 高 | 低 | |
模式 6: 微服务 | 高 / 最高 | 最低~ 高 | 低~ 高 | 低 |
模式 6 有两种形态:RichClient 和微服务(我个人觉得这两种没区别,更多是一个营销噱头,故合并为 6)。RichClient 是一个广为大家熟知的概念,它往往意味着一个 SOA 的 Client 并非一个纯粹的接口或者没有任何多少业务逻辑的类,而是有较多逻辑的。这些逻辑往往和业务系统耦合性较低,但和 SOA 端耦合较高——这种模块结构能简化业务系统开发的复杂度。模式 6 的另外一种形态在 Spring 四大神器中得到淋漓尽致的体现——Richclient 的服务器端往往提供的是数据,本地模块是数据执行环境——四大神器的数据不是来自于服务器端,而是来自于业务系统本身。devtools 通过感知 class 变化提供了热加载能力;autoconfiguration 感知业务系统组件,提供动态组件创建服务;actuator 获知系统运行情况,提供系统运行服务——这些同样是业务逻辑所不关心的,能大大简化业务系统开发的。
我并不认为模块的数据来自哪里是这两种模式的本质特征,我认为和业务系统的职能分工是它们的本质的和真正价值所在。如果将来再出现一种模式,部分数据是来自本地,部分数据是来自 SOA 我们该如何区分呢?那些认为 Springboot 是微服务 Dubbo 不是微服务的,Restful 就是微服务 RPC 就不是微服务,在 Dubbo 仅仅写几个扩展类改成 Restful 以后,springboot 通过 foeign 封装为,该如何分辨呢?微服务 /Richclient 的核心价值就是简化复杂业务系统。在使用中,在 满足业务要求的前提下选择最优的模块形态 。
以某大型互联网公司加解密系统为例,其业务核心要和大量不同机构对接,有大量加解密需求,架构演进如下:
一般而言,在性能、可用性等要求满足的情况下, 我同时会考虑复杂度最低的形态 。能用 api 解决不会用一个类,能用一个类解决的不会用一个模块,能用一个模块解决的不会用 SOA、微服务。能不开发的,就不开发。
在模块实现形式上,我同样发现有很多错误的方式 :
譬如有些公司公司将高度变化的业务系统划分成多个独立的模块,每个部分都是按照某种理论或大师的说法或知名公司的最佳实践等来设计的,理论上和其他部分可以保持很好的隔离性。但实际情况是来一个业务需求 N 个业务新系统一起变化,并且修改的代价是系统级的,联调是 N 个系统级的。本来很简单的事情,做的非常复杂;本来完整的概念,切割的支离破碎,无人可以掌控。
要解决这个问题,我建议的做法进行业务的概念模型设计,将所有的变化都抽象为概念的某些扩展或者属性的变化。为开发人员构建一个完整的业务概念和业务变化视图,给出全局观。在系统实现上区分不变的引擎部分和变化的客户化部分。从这个角度去理解模块选择,而不是基于技术或者理念本身。
因为模块的本质不过是整体和局部的关系,并且同样模块要关注耦合和内聚性。从这个本质去理解模块的设计就要求我们必须如此,从业务出发,从整体出发,从业务模型入手,模块形式只是这个理念的一个具体技术落实手段,并非关键,沉迷于此,不可能得到最优最合理的解决方案。
通过系统划分、模块拆分将复杂度分而治之,但不可能做到简化复杂度。拆分同时会导致问题碎片化、理解和综合困难增加、协调成本提升、复杂性增加,饮鸩止渴当适可而止的。取舍和抽象是效果更好的办法。取舍能避免关注次要问题,降低复杂度;抽象能做到简化复杂度、提升内聚性、灵活性、可扩展性、做到形散而神不散。
DDD 是一个包含了抽象和设计方法论的方法。·网络上能搜索到的对 DDD 的研究可以分为三个层次:
这三块并非一个有机的整体。层次 1 研究问题域的抽象、内聚性和划分问题和组合结构,是 DDD 的核心内容。层次 2 研究代码本身的角色和组织方式,是 DDD 理论在代码书写形式上的应用和最佳实践,它抽象了任何软件的组成部分和构造关系,研究更合理优雅的设计。层次 3 是部分问题域情况下的最佳实践,并非普适的;不加区分的去使用往往是有害的。
在层次 2 中,一般我会比较认为六边形理论更普适更合理但对人的要求较高,它更能适应现实世界的概念之间毫无层次但是彼此耦合的情况。层次模型更简单易用但不普适于问题,但对人的要求较低,适合做大规模项目的管理方法的延伸。
在代码的角色划分上,我个人理解和书中所写也略有偏差。对于数据系统,我认为有这几个角色就够了——接口层、逻辑对象、值对象、实体对象、基础设施层。结合数据系统模型你就会发现:
有很多人反映聚合根很难理解,结合数据系统模型和下图,你就会发现很简单。聚合根就是实体对象(实体对象也可以再找出一个或者几个聚合根)。
从业务逻辑的角度来看,业务逻辑可以写在逻辑对象中,也可以写在值对象(即配置对象—业务或者产品配置等),还可以写在实体对象中;如果业务逻辑写在实体对象和配置对象中,就称为充血模型;如果全部写在业务逻辑对象中,则配置对象和实体对象退化成贫血模型。
判定业务逻辑适合写在业务逻辑对象中还是值对象 / 实体对象的依据就是和这些对象的内聚性关系。并且业务逻辑对象和配置对象的边界是模糊的,如果你将变化表现为配置,它就是配置对象;如果你将它写成业务逻辑,它就是逻辑对象。他们真正不可逾越的边界是业务逻辑对象可以和基础设施层发生依赖,可以和任意对象发生依赖,而值对象不可以和基础设施层和接口层发生依赖。另外如果值对象和实体对象有一个充血方法,放在两者中皆可,我们建议放在值对象中。实体对象也和值对象是同样的情况——至少在 Java 中我们推荐这么做,在一些动态性更强的语言比如 Ruby 中,实体对象可以轻易做到使用基础设施能力透明的提供更加友好的充血方法,这样做更好。
关于层次 3,只是特定场景下的最佳解决方案。DDD 的精髓并不在这里;很多 DDD 拥趸喜欢用这里的某些理念去实施 DDD,常常没有较好的结果,根本原因就是过重,削足适履。
系统整体上可以分为 2 个模块部分:
传统上一般将软件分为三类:居于最底层的操作系统,居于最上层的业务软件和介于其间的中间件。如果我们按照需求频率、开发周期和系统腐化程度(掉坑系数,戏称,即陷入研发焦油坑的可能性)去看的话,会有如下一个表格:
变化频率 | 开发周期 | 掉坑系统 | 通用性 | |
---|---|---|---|---|
应用软件 | 10 | 1 | 10 | 1 |
中间件 | 5 | 5 | 5 | 5 |
操作系统 | 1 | 10 | 1 | 10 |
那么,在中间件和应用软件之间是否可以存在一层称为业务中间件层的东西,它可能既有中间件的较高的软件质量,较低的掉坑系数,又兼有业务软件快速响应业务、开发周期短的优点呢?
如果我们将传统的业务软件分为业务软件引擎 + 业务配置两个部分,在业务配置不用关注业务通用性和复杂性,耦合性,自然就可以有较低的业务响应成本,就可以在总投入不变的情况下,将这些收益转入到个更好的业务软件引擎投入部分,从而做到更好设计、通用性、软件质量。但是层级多了同样会增加组织架构和协作的复杂性,所以如无必要,勿增实体。
以金融 IT 行业为例。最初金融 IT 是掌握在一些行业专业公司手中。银行的科技部门是一个能力较弱的支撑部门,对系统的掌控能力是非常弱的,对业务的支撑也是较弱的。乙方具有较强的业务建模和开发能力,软件具有较高的通用性,但不会考虑太多过于复杂和灵活的客户化需求。
随着互联网的发展,市场上产生了充裕的 IT 人员,这既导致市场上更多的行业公司出现,也导致银行的科技部门壮大,科技部门和行业公司共同满足业务部门灵活而多变的需求。
近些年来,随着银行、互联网公司技术投入日渐增加,科技能力溢出越来越明显,IT 投入成本压力也越来越大。已经出现很多银行将内部的研发中心独立成科技公司,很多互联网公司也在寻求科技能力输出。这种发展趋势的结果会导致行业软件重新变得更加通用化。但不同于之前,这些通用化的行业解决方案同时具有非常灵活的扩展能力、较低的客户化成本、快速的业务响应能力。
金融 IT 的这种发展历程像极了胖瘦客户端发展的历程。最初是胖而不灵活的 CS 模式,然后发展成了灵活的 BS 模式;最初的 BS 客户端能力是非常弱的,最终变得非常强大非常类似 CS,同时兼具 BS 的灵活性。
金融 IT 行业也发展的好像回去了,但是本质已经完全不同了。
不单单是金融,所以行业软件格局的这种发展都应该会符合这个规律,变成如下的样子:
在不远的将来,行业解决方案软件公司可能将重新兴起,业务软件将不像现在这样是需求的堆积,而是高度模型化、设计化的产物。
三离四化是:
为什么要业务和技术分离,业务和部署分离?
答案:风险、成本、灵活性。
我们之前的文章里谈到了很多技术选择的风险,这本质是人的风险。系统肯定是团队合作的产物,也一定不是一个个足够理性的人,技术决策风险是不可避免的。一方面,我们需要规避这种风险。如果业务层和技术层隔离了,我们既能避免这种业务风险,又能鼓励技术人员技术创新。
在内容上,如果我们剥离了技术、部署的部分,理论上业务内核只是一个纯粹的业务 DSL,体积是最小的,开发成本是最低的,灵活性是最高的。
当然纯粹的业务 DSL 只是一个原则,这只是一个理想状态,完全屏蔽技术本身就是一种不切实际的思想,也没有多少实际意义,我不必需要的的业务核心可以随意的在 Java 和 Python 之间实现之间透明切换。在选取最大技术公约数的情况下,我们可以着重的关注一些高频的真正有意义的变化。这样能最大化降低我们的成本。
人与人,与组织机构之间的一些问题同样会影响软件工程,并且它的影响可能要远远大于技术,这是组织域和组织熵,里面可能涉及到一些技术型的解决方案,可能涉及到一些政治性的解决方案,这些非架构和技术仁和园可控的的东西就不详细赘述了,但是必须明白和注意。
一个公司的核心技术框架一旦确定,变化的可能性不大,微观的变化也是允许的。并不是一个特别大的问题。我们也同样无需将我们的业务系统按照高度抽象的原则,设计的对任何变化透明——只需要对存在的变化的需求透明就可以了。有些理论上存在的变化但是实际上并不会发生,那就是不变的部分,可以写死。只需要将变化的部分分离出来,做成可配置,可扩展的就好了。这是变与不变分离。
业务系统中间件化(引擎化)是即是不变的部分,客户化(参数化,脚本化,流程化)即是变化的部分。这样引擎的 开发成本是最低的 , 需求的响应成本是最低的 。 系统的整体质量和稳定性是最高的 ,风险亦可控。
配置中心:业务系统和环境变化之间的配置存储、同步的框架或者系统。
参数中心:维持业务系统业务概念,概念之间关系;以及业务配置和业务系统之间存储、同步的的系统。如果做到了业务系统引擎化|领域化,参数中心即业务需求,即中台。对于大型分布式系统而言,一个统一而独立的参数中心对于维持完整的业务概念、处理灰度、版本变化、同步等非业务型技术问题是非常有帮助的。不同于配置中心,它应该是可运行时 lazyload 的,业务相关而非环境相关的;它的数据结构、数据关系要复杂的多。
这部分的内容本来应该出现在《系统到模块的分解》这部分后面,来论述模块之间的关系。但是又觉得不合适——不仅仅是模块,而是大多数的东西之间的关系都应该服从这样的关系。
正交是一个源自数学的基本概念,比如欧几里得几何 5 大公理是彼此正交的。他们的 组合 方式会产生无穷无尽的定理——能完美解释任何空间结构。物理上时间,空间,动量,能量,他们彼此也是正交的……
在一个完美的软件体系中,也应该用正交的概念来构建。
软件的基本概念就是:概念、概念之间的依赖、以及变化、不可知。
软件工程的基本概念就是:软件,技术,人,它们之间彼此的作用、以及变化。
任何一个领域也应该用正交的概念来构建。
DDD 的代码角色理论:接口,逻辑 ,实体对象,值对象,基础设施,事件对象。
交易领域:买家,卖家,商品,金额。
三离四化。
……
接口之间应该是正交的,对象之间应该是正交的,模块之间应该是正交的……(本质上这几个是一回事,对象是小的系统,模块;系统、模块是大的对象)。
我把上面这套理论叫做业务系统中间件化。
我在 14 年前后萌生了业务系统中间件化的基本想法。之前已经做了接近 10 年业务系统,中间也做过中间件,觉得中间件没有挑战就转回业务系统线了。业务系统需求是一个高频事件、中间件系统需求并非一个高频事件。我喜欢解决能看到现实意义的,做能给团队带来效果的问题。
你能看到的大多数业务系统的质量都是极差的,没人愿意接手的,即使让开发中间件过来的人来做业务系统也没用 ,这本身说明业务系统设计是一个高难度的事情。但是能解决这个问题一定很酷。
之前研究过很多架构理论,但是大多数都是第一页看不完就丢一边了。因为我的偶像爱因斯坦也是这样的,传说别人拿新理论给他看,他往往瞄一眼就丢一边,说:“太丑”。作为一个爱好数学、物理和哲学的外行,我的学术水平和素养虽然没上去,但是欣赏水平和脾气却上去的非常厉害。我相信好的架构设计方法一定是简单的,直达本质的。
寻找办法的过程是很盲目的。就一直这么兜兜转转。经过很多灵光乍现和试错,然后就有了这些东西。最喜欢的事是:最后发现这些规律都太简单了,太容易使用了。然后有 2 年左右的时间,一直在致力于这套理论的实践。
实践的效果,是比较惊人的,一般我能够在一天或者几天之内了解一个遗留系统,重构过的软件代码一般是原系统的 1/3~1/6 甚至更多,扩展性,稳定性极大提升,业务支持能力和响应效率提升数倍;开发时间缩短一半以上。团队成员反映成长极快。但并非全部结果都令人满意:有大约 50% 是结果很好,我完全满意,有大约 50% 有各式各样的问题,不能让我满意。能全部都好反而不正常,技术和设计不是 superman 能挽救一切。你大部分的时间,都不是在解决技术的问题,而是在解决人的问题、公司的问题……虽然我其他水平不行,没有完全解决各种问题,但这已经是我现在能做到的了,我非常满意这个结局。
所有过往,皆为序章。所有将来,皆是可盼。
作者简介:王林,软件浪人。从事软件研发 10 多年,在各种类型 IT 企业包括咨询、物流、电商、金融……服务过,注重研究实际业务问题。不懂高并发、synchronized 底层原理等,写过单节点 TPS 过 w 的分布式风控系统; 不懂微服务、各种架构设计理论,重构的软件代码行数平均是原代码行数的 1/3~1/6,开发效率、稳定性、扩展性等提升几倍而已;没有做过复杂的业务系统,善于把复杂系统做的非常简单。