浅谈交易链路中的一些设计原则&模式 - 阿里技术
阿里妹导读
作者对设计原则、模式等学习后,通过本文谈谈自己的感受。
序
最近在读之前简单看过的书,其中一本就是《企业应用架构模式》,本想写一下读书笔记,但是写的时间是03年的,有些久远了,可能系统结构也翻天覆地,不一样了,摘抄出来感觉也很古老,共鸣没有那么大。不过当时读的时候的内心的宁静还是还是很令人眷恋的。
转过头来,本人之前也对设计原则、模式等进行过学习,但是主要是走心,谈了一下自己的感受。要想获得内心的宁静,感觉还是要参考书中的逻辑,和日常工作中的一些理解进行连接。于是乎,基于这些原则和部分模式简单谈一下。
设计原则
单一职责原则**
定义:
单一职责原则(SRP:Single responsibility principle)又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。
案例:
不同的业务活动有不同的服务入口,无论是履约系统,还是逆向退款系统,都有较多的业务流程bpm。这样做的好处是,可以较好地划分场景,不同场景下接口的限流、错误定义、流程设计、回归测试等都可以独立发展,影响面也比较确定。
如果使用一个通用服务,里面路由很多子服务的话,虽然看上去可以做一些通用的操作,但是相互约束和掣肘会多一点,而且切面也能解决一些通用问题,没有特别大的必要。
**延伸:
值得进一步探讨的是,虽然入口层的隔离比较容易形成共识,但再往下,流程、流程节点、能力、扩展等是否还允许不同场景共用?
实操时,往往会看能力的差异度、场景的复杂度,并基于开发、维护成本综合考虑,不同系统风格还不太一样:
- 履约系统 往下会按能力程度角度沉淀,在一个能力里面需要考虑多种场景,比如:在打款扩展点的时候,可能来自确认收货,可能来自退并打,也可能来自定金罚没;
- 逆向退款系统 的扩展,尽量会按照业务活动独立,比如退款的超时定制,买家申请退款、卖家同意退款、买家退货等不同业务活动有各自的扩展点。
独立,虽然影响面会减少,但是修改的时候,可能因为独立的过多,而漏掉一些场景,凡事都是具有两面性的。好在我们做决策的时候,面临的场景往往是具体的、可数的。
单一职责原则理解
开闭原则**
定义:
开闭原则,在面向对象编程领域中,规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。
案例:
无论是扩展框架TMF的迭代,还是后面星环体系的提出,一个重要的目的是为了解决“业务和平台的隔离”,这也是开闭原则的重要体现。核心逻辑应该由比较熟悉的平台人员进行控制,应该尽量通用,较少修改;扩展逻辑应该由业务开发人员理解,应该尽量灵活,便于调整。
往系统内部看,其实还有很多域能力的扩展,比如:支付的时候可以走直连支付宝,也可以经过支付系统链接到微信等非支付宝渠道。这样的扩展,也是开闭原则的体现,只是离核心流程更近,影响面更大。
往扩展外部看,即使是业务APP包、产品包等插件内部,还是可能会服务多个行业、多个场景,可能有很多的再次路由与扩展。比如:淘系要服务很多行业,服饰、家电、美妆等,不同业务定制也不太一样,往往会用一些策略、责任链的扩展模式。
可见,每个层次都可以设计自己的扩展机制。
延伸:
针对星环的扩展层次,有几个可以进一步考虑的问题。
1.业务隔离的机制
针对隔离出来的变动部分,我们其实有更多的期待:期望不同的业务、维护者之间还能互相隔离。
虽然,在星环的体系里面,用业务身份的概念做了隔离,不过这只是个技术角度,把场景冲突尽量前置到了解析层面,减少了后续执行的压力,使用时并不太灵活。比如:圈品时还没有下单的“业务身份”,会按商品标等标识圈品,会横跨多个“业务”。好在技术上还有“产品包”的方案,可以叠加业务,实现逻辑复用,但也经常出现“漏叠”的场景。
但如果,不提供业务身份的概念,基于请求场景判断,那么影响面和表达上又会充满不确定性,也难以解决冲突时“谁”和“谁”冲突的判定。
**2.业务和平台的边界
我们常说基础域,其实可以认为是基础 + 域,因为除了域能力和扩展等,业务流程、商业能力、base实现、平台 share、common包、开发SDK等,我们都认为是基础的,需要平台人员参与。
但是,往往会出现一些特例:
- 在域扩展里面,针对一个特殊业务提供能力(这个业务的逻辑比较完整和独立),可以不走到扩展点。会出现,长在平台的jar包中,但是演变规则基本是业务定,开发又需要平台介入的特殊合作情况;
- 在独立部署的系统中,代码库进行fork出去,进行独立演进。这时,整个层次都会被定义为“业务”;
- 商业能力被认为是平台能力,也会集成进平台sar包中,但是像税费、进口商业能力,基本是国际业务维护,很难说是平台逻辑。
从这些例子来看,业务和平台的边界已经超越了具体层次的定义了,不再那么绝对。核心还是跟着“权责一体”的方向发展。
开闭原则理解
里氏替换原则**
定义:
里氏代换原则 (Liskov Substitution Principle LSP) 面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
案例:
可替换的思路,我们经常在使用:
- 在进行扩展点定制的时候,我们并不关心是业务包还是场景产品包返回的结果,只关心定制的结果。
- 在进行数据库切换的时候,我们并不关心走向哪个数据服务,只关心返回的结果;
- 在外部支付系统调用的时候,我们并不关心走的是支付宝还是微信,只关心支付的结果;
- 在进行订单查询的时候,我们并不关心走的是订单库,还是外部服务(如评价),只关心查询的结果;
- …….
可替换的原则,让我们面向抽象编程;替换时能否足够顺畅,取决于我们的抽象是否合理。
**延伸:
虽然我们可以进行抽象,做到可定制替换,但是,事实上往往很难做到无感:
- 服务保障可能不一样:比如待付款、待发货等查询的是订单库,待评价查询的评价接口,接口的保障等级和能力可能不一致。需要做额外的稳定性保障。
- 实现能力可能不一致:过了3个月之后,订单会进入历史库,虽然查询层面可以做适配,做到消费者无感,但是消费者后续操作会受到制约,因为变更的时候,两边能力是不一样的。有些按钮会在进入历史库后降级。
- 协议可能不一致:比如支付系统的替换,支付宝因为有担保交易,所以售中退钱比较快速方便,但是微信登渠道,受到后面资金托管和策略影响,退款可能没有那么及时。两边的错误码等也是不一样的。
- …….
里氏替换原则理解
迪米特法则**
定义:
迪米特法则可以简单说成:talk only to your immediate friends。对于OOD来说,又被解释为下面几种方式:一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
案例:
业务活动的执行过程,是一次协调数据操作的过程,最终大家达成一致,落库发消息。在这个过程中,为了协调各个领域的协作,有一套基础流程和对应节点组成的协调层,这个层里面最典型的协调者就是上下文(Request、Result、Context 等概念)。数据从上下文里获取,如果要传递给后续节点,还需要塞回上下文,称之为回收。
从更大的角度看,入口系统也是协调者的角色。比如,下单系统会调用商品系统、库存系统、营销系统、资金系统、履约系统等,进行数据的收集与传递。系统之间很少会直接互相调用。
这样的协调者,往往做的是调用和convert的操作,但是正因为有了这层convert,也便有了理解和管控:
- 可以精简模型,减少链路上数据传递和多次convert;
- 可以控制只读,避免后续进行非预期的篡改;
- 可以节约性能,可以设计懒加载等模式,需要时再真正获取;
- ……
**延伸:
协调者因为需要携带各个参与者的信息,随着参与者越来越多,会变得越来越厚重。而且因为有些系统层层次较多,也会被层层covert所折磨,每次新加个数据,都需要都加一遍。于是,渐渐的,大家开始使用了共享模型,携带了相对原始的数据。
这样的结局背后,就会引发另一种想法:每个参与者提供一个固定的区域,来获取原始数据,面向数据中心来玩,是不是就可以绕开协调者层层透传了。而且数据中心也应该知道如何更好地管理自己的数据。
如果你的系统层次之间,只是convert,那么的确这会方便得多,但是如果你的系统之间当中还有一些隐晦的过程逻辑,可能会加工这些数据,那么就已经超越数据中心的范围了。此外,如果你有聚合根的设计,某些部分是一块整体,分散的数据中心的一致性也很难操作。
最后,更主要的是,协调者本身是为了协调,那么肯定是“知名”的。对于开发来说,是上下文更容易找到,还是各个分散的中心更容易找到,这也是很重要的点,采取分散的方式是需要有一定规约的。
迪米特法则
接口隔离原则**
定义:
客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。使用多个专门的接口比使用单一的总接口要好。一个类对另外一个类的依赖性应当是建立在最小的接口上的。
案例:
接口隔离常见的case有:
- 按读写能力隔离:读数据的接口一套,写操作的另外一套。
- 按操作角色隔离:买家操作的接口一套,卖家操作一套,小二操作的一套。
- 按页面类型隔离:PC的一套、H5的一套、客户端的一套。
- 按组件协议隔离:奥创的一套、Astore的一套、DTO的一套。
- ……..
看到这些场景,我们天然会想到要隔离,代码本身也大概率在不同模块中。接口的隔离,不仅仅是声明方式:
- 对客户端来说,依赖也可以变小(虽然每个系统往往只有一个大client),可以排除一些不必要的依赖;
- 对服务端来说,也可以更好地独立发展,避免耦合,对于复用的也可以抽象share和common。
**延伸:
在订单管理系统中,有一个接口是doOp, 定义了按钮的操作,通过传入的 操作code 不一样,可以进行“提醒发货”、“取消订单”、“删除订单”、“延长收货”等各种操作。这样做的背景是,订单的按钮可能多达上百个,定义接口,不仅仅是服务端的事情,还需要申请无线的包装接口mtop,客户端也要继承,为了尽量复用到客户端的通路,提供了一个比较通用的接口。
这里,可以看到,接口独立的原则并不是绝对的,和要抽象的数量、之间的相似程度都有关系。此外,上面按钮的例子也不是"绝对的不隔离",只是入口层的复用,后续还是按照按钮code严格正交的,会根据按钮code路由到不同的处理策略。
接口隔离原则理解
依赖倒置原则**
定义:
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
案例:
如果认为基础服务在短期内不会改变,也没有多套实现,往往会直接按调用链路中“上层依赖下层”的逻辑去依赖,这样会非常简洁高效。比如,订单管理系统里面对订单查询的服务,当做Repo,作为底层服务,在域内是直接调用实例的。
如果认为服务是外部的,不受到自己的管控,要隔离变化,保留升级接口的能力,那么往往会再包装一层接口。在下单和履约系统 里面是有 网关 gateway 的概念的。变成了依赖了抽象服务接口,不感知具体的实现实例。
加一层抽象接口进行解耦,会保持较好的松耦合能力,因为接口就是抽象契约,两边可以独立发展,但是也会带来管理的成本,这是一种判断与取舍。
**延伸:
虽然概念上这个层次很好,但是做到位还是有一些成本的:
- 打包模块:假设在A依赖B的过程中,引入了抽象C。这样的抽象层,因为和A,B没有关系,应该是单独的jar包和代码库。但是常常因为新建库的麻烦,会托管到A 或 B 的某个子模块中,打包的时候需要单独打一下,比较别扭。
- 复杂对象挑战:面向抽象的接口,意味着更多的convert,在普通系统中,可能这是相对轻松的,但是在交易复杂对象设计的背景下,这又将是一个痛苦的过程。更加可怜的是,交易系统的领域对象与数据库模型是一个逻辑映射,叠加这些层次后,很难找到数据是怎么带出来的。
所以,有时候,会反向而行,选择紧耦合的模式,在一个复杂的系统中,往往会有这样的感觉:简单、纯粹、紧耦合才是一道曙光,因为点点点,就能找到相关代码,而不是点点点…….点点点,就迷路了。
这么说,并不是唱反调,希望能够辩证地去看待问题,结合具体的场景,有舍才有得,有得必有失。
依赖倒置原则
设计模式
下面选取23个设计模式中的一些进行一下介绍。
模板(Template)
模版方法是说对一个执行过程进行抽象分解,通过骨架和扩展方法完成一个标准的主体逻辑和扩展。
交易链路上平台和扩展能力的设计,做类比是比较合适的。基础的模版就是整个流程的编排和对应的节点,可扩展的地方就是各种业务定制区域。这样形成了平台和业务较好的融合。
责任链(Chain of Responsibility)
责任链是说将请求让队列内的处理器一个个执行,直到找到愿意执行的。
商业能力扩展、域扩展,在执行回收结果的时候,会遍历实现的插件,并结合回收规则,进行及时的熔断。这和责任链的逻辑是类似的。以确认收货打款时“是否跳过通知支付”为例,TMF执行引擎会遍历产品包、App包的实现,找到第一个返回要 true(跳过)的结果时,就会停止执行,整体返回 true。
策略(Strategy)
策略是说完成一个事情有不同的算法,可以进行相关切换。
在逆向退款中,需要支持不同的退款链路,有些需要是担保交易,有些是保证金链路,有些是微信支付,有些是退卡、退资产。为了支持多种出账策略,采用了策略模式,可以通过扩展点定制各种资金策略,同时可以执行单个,也可以执行多个。
观察者(Observer)
观察者模式是说我们通过注册、回掉这样的协作设计,完成变化通知的协作机制。
交易中,系统内部的观察者模式不多见。但是系统间基于消息的观察模式还是很多的。比较典型的有逆向的0s退:通过监听退款创建的消息,进行同意调用,实现了0s退的快速同意功能。通过消息的异步通知方式,既可以较好地进行解耦,也可以在失败时利用消息的重投机制,增加成功的概率。
状态(State)
状态模式是说在不同的状态下,有不同的处理行为。
交易系统中引入了工作流,会定义业务活动可以经历的状态,每个状态可以进行的操作。比如:普通担保准交易流程,就包含:创建外部支付交易、付款回调、创建物流单、发货、确认收货 这些状态节点。每个节点也定义了可以进行什么操作,比如在 创建外部支付交易 这个节点,就可以执行支付校验、关闭订单、修改价格等操作,但是不能进行打款、退款等操作,因为还没有付款。
中介者(Mediator)
当多个类之间要协调的时候,往往引入中介者进行协调,减少大家的知识成本。
交易系统中的流程执行过程中,会有一个大的上下文,这个上下文会协调各个领域的数据。比较典型的一个场景是,各个编排节点都可能会影响到数据更新,需要有一个地方存起来,然后交给最后的更新节点。这个传送信息的角色往往就落到了上下文这个中介者身上。下面是逆向流程中更新协作的一个大致结构。
组合(Composite)
组合通过继承的模式,和孩子节点,可以递归地去描述一个对象层次。
递归的思想,一个比较好的理解例子是下单系统中的拆单,将一些列的订单,不停地分组。在逻辑上理解,就像递归地去进一步细化一样。
单件(Singleton)
单件是说在多线程的情况下,要保证对象只创建一遍,作为独一无二的资源。
在订单管理系统中, 外部调用服务都被命名为Repo, 作为一个资源库。为了方便的获取这些资源库,都通过单例的模式去获取,这样一些工具类也可以方便的通过静态方法调用服务,而不需要注入bean。这样的Repo有:订单服务、评价服务、图标服务、超时服务等。
解释器(Interpreter)
解释器是说针对一套上下文,形成一套语言,可以通过解释表达式含义的方式完成对应的任务。
交易中见到的解释器模式主要是,原来淘系的牛顿系统,一个动态脚本类配置。这个配置平台主要解决产品包中的一些动态规则,通过推送的模式,可以利用解释的动态性,减少一些部署的成本。
代理(Proxy)
代理是为了包装一个类,对相关操作进行二次转发或者进行一些管控。
订单管理系统中, 为了避免上下文被各个域篡改,对上下文是有一定保护措施的。当进入到具体执行节点的时候,会进行上下文转换,转换过程中,会通过包装只读接口,去代理实体对象,提供只读服务,而获取不到具体实例,也无法进行set修改。
总结
《企业应用架构模式》中有一句对模式的描述,写得比较好:
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。
本文借助了一些原则和设计模式,讲了一下自己窥探的交易系统中的一些设计。希望能够给大家一个视角,多了解一下我眼中的交易链路。
推荐:
-
《设计模式六大原则理解》
-
《DDD的关键理解》