07 | 可复用架构:如何实现高层次的复用?

你好,我是王庆友。在前面几讲中,我们讨论了如何打造一个可扩展的架构,相信你对架构的可扩展有了一定的了解,而架构还有一个非常重要的目标,那就是可复用。所以从今天开始,我就来和你聊一聊,如何打造可复用的架构。

作为开发人员,你对复用这个概念一定不陌生。在开发过程中,我们把系统中通用的代码逻辑抽取出来,变成公共方法或公共类,然后在多个地方调用,这就是最简单的技术上的复用。

但一开始,我们不会过多地考虑复用,当一个新项目过来,我们会选择最直接的方式来实现,结果往往是欲速而不达,比如说:

  • 好不容易搞定了一个项目,接着又有新的类似项目过来,我们又要从头再来;
  • 项目的代码是定制的,项目结束后,系统维护的噩梦刚刚开始。

如果项目缺乏沉淀,每个项目都是全新的开始,出现这些情况,一点都不意外。而要想解决这个问题,我们一开始就要考虑系统的复用性。

复用,它可以让我们站在巨人的肩膀上,基于现有的成果,快速落地一个新系统。

那么,我们在做架构设计时,如何实现系统的高可复用呢?

今天,我就针对复用这个话题,首先和你介绍一下,复用具体都有哪些形式;然后,我会针对最有价值的业务复用,带你了解如何划分服务的边界,让你能够在工作中,设计一个可以高度复用的系统。

复用的分类

复用有多种形式,它可以分为技术复用和业务复用两大类。技术复用包括代码复用和技术组件复用;业务复用包括业务实体复用、业务流程复用和产品复用。

从复用的程度来看,从高到低,我们可以依次划分为产品复用>业务流程复用>业务实体复用>组件复用>代码复用。

接下来,我就按照复用度从低到高,对这些复用方式进行一一分析,帮助你更好地理解架构的可复用性。

技术复用

首先是代码级复用,这部分应该是你最熟悉的了。这里包括你自己打包的类库,第三方提供的SDK,还有各种算法封装等。我们的代码可以直接调用它们,物理上也和我们的应用打包在一起,运行在同一个进程里。代码级复用是最低层次的复用,你可以把它当作你自己源代码的一部分。

再往上,是技术组件复用。这些组件有我们自己封装的,更多的是大量开源的中间件,比如Redis、MQ、Dubbo等;组件也包括各种开发框架,比如Spring Cloud。这些基础组件技术复杂度很高,它们的存在,极大地简化了我们的开发工作。

值得注意的是,代码级复用和技术组件复用都属于工具层面,它们的好处是在很多地方都可以用,但和业务场景隔得有点远,不直接对应业务功能,因此复用的价值相对比较低。

业务复用

我们知道,系统最终是为业务而服务的,如果能够实现直接的业务复用,那系统开发的效率就更高。在前面的课程中,我们讨论架构的演进过程时,很多地方谈到了业务能力的复用,比如说,微服务强调单个业务实体的封装和复用,而中台进一步实现了企业级业务能力的复用。

所以接下来,我们就从比较简单的业务实体复用开始说起。

业务实体复用针对细分的业务领域,比如订单、商品、用户等领域。它对各个业务领域的数据和业务规则进行封装,将它变成上层应用系统可以直接使用的业务组件。

业务流程的复用针对的是业务场景,它可以把多个业务实体串起来,完成一个端到端的任务。比如说,下单流程需要访问会员、商品、订单、库存等多个业务,如果我们把这些调用逻辑封装为一个下单流程服务,那下单页面就可以调用这个流程服务来完成下单,而不需要去深入了解下单的具体过程。相比单个的业务实体复用,业务流程的复用程度更高,业务价值也更大。

最高层次的复用是对整个系统的复用,比如说一个SaaS系统(Software-as-a-Service),它在内部做了各种通用化设计,允许我们通过各种参数配置,得到我们想要的功能;或者说一个PaaS(Platform-as-a-Service)平台,它会提供可编程的插件化支持,允许我们“嵌入”外部代码,实现想要的功能。

这种产品级的复用,它的复用程度无疑是最高的。这样的系统,在落地的时候,它无需核心的开发团队进行开发,只由外围的实施团队负责就可以了,这样,一个项目的上线就能简化为一次快速的实施,不但上线周期短,系统也更稳定。

当然,实现这样的复用,难度也是很大的,你既要对所在行业的业务有很全面的理解,又要有很强的抽象设计能力。这类系统中,比较典型的有Salesforce的CRM系统和SAP的ERP系统。

现在,我们先对复用做个总结。从技术复用到业务复用,越往上,复用程度越高,复用产生的价值也越大,但实现起来也越复杂,它能复用的场景就越有限。在实际工作中,技术层面上的复用相对比较简单,我们对这部分的认知也最多,而且由于开源的普及,现在有丰富的中间件让我们选择,我们可以基于它们,逐步构建适合自己的技术体系。

但如果我们能进一步打造业务中间件,并在这个基础上,形成业务平台,这样,我们就能实现更高的业务级复用,可以更高效地支持系统的快速落地。

而在实现业务组件化和平台化的过程中,首要的问题就是基础服务边界的划分。边界划分决定了服务的粒度和职责,在实际工作中,也是非常困扰我们和有争议的地方。

接下来,我就针对基础服务边界的划分,和你分享我自己在项目开发的过程中,总结的一些实用的原则和做法。

基础服务边界划分

服务边界划分要解决“我是谁”的问题,它实现了服务和周边环境的清晰切割。

我们都知道,服务包含了业务数据和业务规则,并提供接口给外部访问,其中,接口是服务的对外视图,它封装了服务的业务数据和规则。

所以从边界划分的角度来看,我们就是要确定哪些数据属于这个服务,哪些接口功能由这个服务提供。这里,我总结了3个基础服务边界划分的原则,供你设计时做参考。

首先,是服务的完整性原则

你在划分服务的边界时,需要确保服务内部数据的完整性。

举个例子,一个商品服务的数据模型,不仅要有商品基本信息,比如商品名称、价格、分类、图片、描述等;还需要包含商品的扩展信息,如商品的各种属性、商品标签等;最后还要包含各种复杂商品类型的定义,比如组合商品、套餐商品、多规格商品等。

另外,你还要保证服务功能的完整性。对于服务使用者来说,他们是以业务的角度看服务,而不是纯粹的数据角度。比如一个套餐商品,在服务内部,它是多个单品的复杂组合,但从服务调用者的角度来看,它就是一个商品。

那现在问题来了,对于套餐的价格,商品服务是给出一个最终价格呢?还是给出各个单品的价格,然后让调用方自己算最终价格呢?我们知道,套餐的价格不是各个单品价格累加的结果,它包含了一定的优惠,如果它的价格由服务调用方来算,这会导致商品的部分业务规则游离于服务外面,破坏了商品服务的功能完整性。

在实践中,有些服务只是存储基础数据,然后提供简单的增删改查功能,这样一来,服务只是一个简单的DAO,变成了数据访问通道。这样的服务,它的价值就很有限,也容易被服务调用方质疑。因此,我们要尽可能在服务内部封装完整的业务规则,对外提供完整的业务语义,最大程度地简化服务的使用。

所以,当你在划分服务边界时,要保证服务数据完整、功能全面,这样才能支撑一个完整的业务领域。

其次,是服务的一致性原则

也就是说,服务的数据和职责要一致,谁拥有信息,谁就负责提供相应的功能。

服务内部的业务逻辑要尽量依赖内部数据,而不是接口输入的数据,否则会造成数据和业务规则的脱节(一个在外面,一个在里面),如果服务对外部的依赖性很强,就无法提供稳定的能力了。

很多时候,我们对一个功能到底划分到哪个服务,有很大的争议。这时,我们可以结合这个功能所依赖的数据来判断,如果功能所需要的大部分数据都存储在A服务里,那当然由A服务来提供接口比较合适,这样接口输入的数据比较少,不但简化了服务对外部的依赖,同时也降低了接口调用的成本。

给你举个例子,在订单小票上,我们经常能看到一些优惠信息,比如说商品原价是多少,其中因为满减优惠了多少,因为商品特价减免了多少。这个优惠计算的结果是订单的一部分,毫无疑问,它需要保存在订单服务里。

但这个订单的优惠计算过程,却不是由订单服务来负责,而是由独立的促销服务负责的。因为优惠计算所需要的优惠规则是在促销服务里定义的,促销服务可以在内部拿到所有的优惠规则,然后完成整个优惠计算。

否则,如果是由订单服务负责优惠计算,订单服务的调用者就需要在接口中提供完整的促销规则,不但调用成本高,而且外部促销规则的改变会影响订单服务的内部实现。

所以在这里,促销服务负责促销规则的维护,以及对应的优惠计算功能;订单服务负责优惠结果数据落地,以及后续的查询功能。这样,每个服务存储的数据和对外提供的功能是一致的。

最后一个,是正交原则

既然是基础服务,它们就处于调用链的底层,服务之间不会有任何的调用关系,也就是说基础服务相互之间是正交的。比如说会员服务和商品服务,它们代表不同维度的基础业务域,彼此之间不会有调用关系。

正交还有另外一种情况:服务之间有数据的依赖关系,但没有接口的调用关系。

比如说,订单明细里包含商品ID信息,但订单服务内部不会调用商品服务来获取商品详情。如果页面需要展示订单的商品详情,针对这个具体的业务场景,我们可以在上层的聚合服务里,通过聚合订单服务和商品服务来实现。

总结

可复用是架构设计的一个重要目标,今天我们对复用进行了梳理,包括复用有哪些形式,以及它们有哪些价值,相信你现在对复用已经有了一个整体的认识。业务上的复用比纯粹的技术复用有更高的价值,我们要尽量往这个方向上靠。

在实践中,落地基础服务是实现业务复用的有效方式,而基础服务边界的划分,它有科学的成分,但更多的是一种艺术,这里我提供了几个实用的划分原则,你可以在工作中结合实际情况,灵活地运用它们。

在专栏的下一讲,我会通过一个具体的订单服务例子,来帮助你更好地落地基础服务。

最后,给你留一道思考题:我们在落地服务时,有时会冗余存储其它服务的数据,你对这个有什么看法呢?

欢迎你在留言区与大家分享你的答案,如果你在学习和实践的过程中,有什么问题或者思考,也欢迎给我留言,我们一起讨论。感谢阅读,我们下期再见。

精选留言

  • 四喜

    2020-03-12 11:45:13

    老师请讲讲订单服务,

    订单小票包含了价格,价格涉及到优惠信息。

    价格信息来自商品服务,优惠信息来自促销服务。订单服务从数据上依赖商品服务、促销服务。

    请问这个订单小票,是订单服务的功能呢?还是上层聚合服务的功能呢?

    订单服务本身负责什么呢?
    作者回复

    这个比较复杂,大家不要陷入这个具体的业务里面。
    具体讲,就是
    1. 上层应用调用促销服务,把购物车的商品信息和用户信息传进去,返回的是最终订单金额,以及因为各个优惠减免的金额。这个是促销服务结合内部的促销规则记性计算
    2. 然后上层应用调用订单服务,把订单的信息,以及优惠结果传进去,订单服务负责在数据里保存优惠结果。

    所以是上层应用调各个基础服务,基础服务之间不会互相调。

    2020-03-12 21:50:02

  • jun

    2020-04-01 14:43:33

    可以冗余一部分其他业务领域数据,但这部分冗余数据得有2个特点:1、不需要实时变化的数据 2、不需要保持数据一致性的信息;
  • 2020-10-12 15:57:25

    可复用架构
    技术复用:工具层面,复用价值较低

    代码复用:算法,SDK之类
    技术组件复用:Redis、MQ、Dubbo
    业务复用

    业务实体复用:业务领域,订单、商品、用户
    业务流程复用:业务场景,下单流程
    产品复用:整个系统,SaaS、PaaS
    基础服务边界的划分:

    完整性原则:要保证服务数据完整、功能全面,这样才能支撑一个完整的业务领域。
    一致性原则:服务的数据和指责一直,谁拥有较多的数据,谁就负责提供相应的功能
    正交原则:服务之间可以用数据的依赖关系,但没有接口的调用关系
  • 果然爸爸

    2020-05-16 08:55:32

    像用户服务这种基础服务,所有其他业务服务都会依赖,按照老师的说法,基础服务最好不要相互调用。这种情况,怎么处理,把用户数据冗余到所有服务吗?这样数据同步会是个比较复杂的问题。
    作者回复

    在基础服务的上层聚合各个基础服务,比如应用通过订单服务拿到订单的用户id后,再调用用户服务获取用户详细信息,而不是订单服务内部去调用用户服务。

    2020-05-16 15:18:28

  • DullBird

    2021-06-06 22:25:13

    商品价格的例子,有点疑问。商品的录入的时候基本价格应该是属于商品域的。但是实际展示的时候涉及到营销活动,优惠活动。这个销售价又属于营销或者价格域的。
    其实这里商品有时候也会冗余最终的销售价(会有一定延迟),这样在搜索商品的时候。基于价格排期才比较方便。
    作者回复

    价格规则复杂情况下,往往有独立的价格服务,比如同一个商品有app价,团购价,pc端价,线下价,促销价等等,涉及复杂的优先级排序

    2021-06-22 16:41:08

  • LiuHu

    2020-04-06 10:37:58

    冗余的目的是保证自身业务的一致性、完整性, 原则是冗余的信息越少越好,不是为了方便搜索查询而冗余一些业务领域外的不必要信息。对于搜索查询,交由专门的聚合查询服务。
    对于可变的信息只冗余关联信息的ID,对于不可变数据冗余可以具体信息。
  • AngryApe

    2020-03-17 14:20:44

    王老师,请教一个问题:在架构设计的时候服务一般的分层的,基础服务层(业务实体),共享业务层,应用服务层,按这个结构的话是否允许应用服务层直接调用基础服务层?比如一个简单的查询优惠券详情,没有什么复杂的逻辑,这时候应用层是否应该直接调用基础服务层?
    作者回复

    可以跨层调,你这里的共享业务层是聚合业务层吧? 基础业务层提供的就是共享业务

    2020-03-17 21:55:07

  • Din

    2020-03-06 09:36:36

    冗余其他服务的数据时比较常见的做法,这样可以减少和其他系统数据的交互,提高服务性能。不过数据一旦冗余,就会带来数据一致性的问题。
  • Middleware

    2020-03-06 13:10:27

    冗余一些数据,字段,我认为适当的场景是很有必要的。检索数据方便,查询效率也会提高
  • 每天晒白牙

    2020-03-06 08:02:25

    冗余数据有时也是可以的,就像电商中有买家查数据的需求,也有卖家查数据的需求,在做分表时为了提升性能会做冗余
  • Geek__b3bddc1474fa

    2022-08-12 17:53:58

    课题是:如何实现高层次的复用?但实际只是介绍了不同类型复用,大篇幅在介绍,没有讲解怎么做到。结论是没有干货
    作者回复

    嗯,专栏这篇文章从实际出发,建议复用层次“高到" 基础共享服务复用就可以,后面针对基础共享服务设计做了很多深入介绍,注意连贯起来阅读和理解。

    2022-09-16 18:59:05

  • 流云追风

    2021-10-24 10:09:34

    订单服务一般会冗余其他服务的部分信息,譬如会员ID和积分,这样在报表查询,小票打印以及订单推送至顾客时不用再访问会员服务
  • ezekiel

    2021-10-09 01:44:46

    老师您好!
    在服务的完整性原则中,套餐商品的价格应该由商品服务提供,商品服务应该访问优惠服务获取信息,然后返回调用方法,但是在下面正交原则中,基础服务是不应该访问其他服务的。这里有些困惑
    作者回复

    这个应该是商品聚合服务,从领域来说,职责属于商品域,商品域包含商品聚合服务和商品基础服务,商品基础服务符合正交原则。

    2022-01-12 16:45:00

  • Geek_0d2e5d

    2021-01-06 08:01:12

    老师讲的太好了,赞
  • Robin康F

    2020-04-21 13:23:13

    冗余数据是比较常见的一种做法,目的是减少查询和交互次数,基本原则是 1数据变化很小 2非核心数据,可以提高性能,但对于变化多、复杂度高、和核心链路的数据冗余,要分析其影响,一般是不能冗余的
  • 密码123456

    2020-04-17 08:32:44

    冗余存储,我觉得是必要的。好处是,减少外部的调用。也有坏处,别处修改,无法感知。但是冗余还是有必要的,可以使用时效性冗余,或者基本不变的冗余。
  • 梁中华

    2020-03-29 11:52:05

    现在很多做垂直Saas的平台属于那一层服用?感觉产品级已经不够用了,需要平台级服用
  • 追忆似水年华

    2020-03-28 15:37:28

    “业务上的复用比纯粹的技术复用有更高的价值,我们要尽量往这个方向上靠”,这句话是精髓,不管用了什么技术,最终都要落地到一个个具体的业务,对大部分开发者来说,日常的工作也是服务于业务。那么为了让业务的开发更高效,就要尽量实现业务层的复用,受教了。
  • ROCKETsFORWARD

    2020-03-15 18:29:36

    空间换时间的做法。有时为了避免跨库join,需要保证查询效率时,可以本地冗余其他服务的数据。一般情况下冗余不常变化的数据。当冗余数据有变化的时候,可以开启定时任务在业务不繁忙的时候进行更新,保证最终一致性。
  • 雨霖铃声声慢

    2020-03-08 10:10:38

    冗余存储其他服务的数据好处是查询效率高,单个服务就能满足业务需求,无序聚合查询其他服务,但是也要注意冗余的度,要综合考虑到数据一致性和性能来做决定。