网站注销备案查询,滨州做网站优化,地方网站建设方案,wordpress阅读积分我从2015年起至今2022年#xff0c;在业务平台#xff08;结算、订购、资金#xff09;、集团财务平台#xff08;应收应付、账务核算、财资、财务分析、预算#xff09;、本地生活财务平台#xff08;发票、结算、预算、核算、稽核#xff09;所经历的业务系统研发实践… 我从2015年起至今2022年在业务平台结算、订购、资金、集团财务平台应收应付、账务核算、财资、财务分析、预算、本地生活财务平台发票、结算、预算、核算、稽核所经历的业务系统研发实践的一个总结。 1.核心是面向复杂性业务支撑的实践经验个人概念里的“复杂业务“大概至少面向5类行业若干业务线且业态差异很大文章不涉及性能、稳定性、资损防控、大数据离线研发聚焦在线业务系统架构对多态业务的包容性、开放性、灵活性、可读性。 2.文章较多强调”个人”两字因为仅是我个人在实践上归纳总结的一些方式方法。 3.实践经验主要来自两类一类是接手旧系统得以见识不一样的设计文中“见过”特指。二类是自己新建的之后又一而再三重构的这类姿势下人的反思记忆尤为深刻。 作者关斌(修觉)
一、单系统内架构形态反面 下面是个人经历过的反面案例 1.1 业务层臃肿能力层单薄 这类形态挺常见的最初设计时做四层划分也比较清晰。最核心的设计点是biz层编排“可复用的”service层完成一个场景逻辑表达。 问题在于
第一Service本身的划分、定位相对随意从第一感觉而来并未经过领域划分这样的设计。所以这里特意叫service从而区别于domain的表达我采用的领域划分方法下文会阐述;
第二Service本身不可扩展多态业务冲击下为适配此service能力而存在的个性向共性的转换逻辑上浮。 因为这些问题的存在随更多业务接入的架构演进下组织扩大后的人员差异扩大下会导致几个问题 biz层越发的膨胀service层越发的萎缩。biz层里充斥了各种本该往下沉淀的可复用业务逻辑service层则几乎萎缩为dao; 人员的差异下service实例颗粒度不一有萎缩有膨胀有重复相似的。biz实例亦是如此; 由于biz膨胀层内会发展为两小层上小层为面向单一业务场景的“业务biz层”下小层为通用可复用场景的“通用biz层”且这两层隐约存在随研发个体认知差异或隐或现。由于service的萎缩biz层会干脆直接调用dao且因人而异的随机性差异导致biz往下的调用关系呈乱麻态。 这个形态我印象最深的是2015年在结算研发时经历过。当时设计理念是biz层通过hjbpm编排底层可复用的service表达业务逻辑甚至试图以可视化界面让业务运营自编排当时的设计理念和初衷都是很好的。在经历大规模多态业务接入的冲击下biz层产生了严重的膨胀导致最复杂最难懂最易出问题的逻辑都堆积在biz层。我们大概在2016年做了分层、分域、SPI式开放化架构升级。 1.2 service间网状调用 这类形态也挺常见的。同样核心的问题还是对service这层的颗粒度、职责定位不清晰对增量service的架构监管不足业务压力下祸起于常见的倒排需求常以需求完整度妥协架构方案妥协追赶deadline死线挖下损害长期利益的坑一线研发同学就容易凭感觉去新增service。 与上文形态1不同的是该组研发未明确和共识biz层的“编排”作用基于原本bizA - serviceA 的实现链路下随新增业务逻辑新起的serviceB事是A的事但不合适放serviceA所以新建链路演变成了bizA - serviceA - serviceB。 这样的趋势持续发展下去会发现bizA下的service调用链路越发的复杂呈现为一颗深度调用树而biz层失去了业务编排的作用退化为一个业务场景入口的标志符。从代码阅读层面讲以为是做A 因为bizA - serviceA的起手势实则带着一串BCDE等等且越挖越深不见尽头。 一个新同学除非步履维艰的推演完成全串调用代码否则不能知晓bizA业务场景真实完全的意义。步履维艰在于这是一颗深度调用树当你阅读到serviceE时很可能你已经忘记了最初是从哪个biz入口看过来的以及接下来你将还会遇到哪些service讲述剩余的业务逻辑。 而一个写的并不那么好的代码还常将这些service的调用关系隐藏在极其隐晦的角落给阅读者增加复杂。譬如通过一个叫util的类完成了service B - C这一主干链路的传递。一个service的改动几乎无法评估其产生的影响面。而因为无法评估又怕改出故障在业务需求压力下会出现各类fork行为加剧加速腐化。这是恶性循环。
1.3 混合态 真实世界里1、2两种形态会同时发生事实上都是混合态只是问题偏重度不同分开阐述便于理解。 二、单系统内架构形态采用 个人在实践里采用的形态: 看起来似乎和前面列的案例没大区别但在实践结果上是有差异的。个人认为核心有2个点 起步时的domain设计有理论支撑的domain划分和凭经验感觉划分的service对整个架构生命力的影响是完全不同的 过程中的架构原则必须有几个易记牢记并坚持贯彻的架构原则见下文的实践细节在实操层面上对整个架构性命的维持是完全不同的。
2.1 细项1 - 分层
✪ 2.1.1 4层定位 api层我比较更喜欢叫应用服务层因为API其实是个协议范畴的语义非实现面向应用层面对外的服务表达。灵活为主薄可随业务、渠道定义独立API biz层业务层面向一种业务场景的逻辑表达如下单。负责业务编排一个业务场景的主流程应在这层直观可视。灵活为主薄可特定场景独立biz类 domain层领域服务层围绕一簇模型操作的逻辑表达例如费用单域、账单域。核心能力的所在地要规范、要厚、沉淀复用为主通过SPI兼顾业务个性化 dao层存储层面向一个存储对象操作的逻辑表达无论是内部存储 or 外部存储如rpc外调。灵活为主可随db差异、性能需求独立dao方法。 ✪ 2.1.2 实践中的细节问题 api和biz层冗余了不是
语义上api层是站在应用的角度与外部应用交互约定的实现是向外表达。biz层是应用承载业务里的某一类场景是向内表达。虽然大多数情况下两者是1:1的关系且api层几乎薄到只做透传但毕竟语义不同也会出现N:M的情况。 举例 业务线共用API A然为B业务专开API B如考虑其鉴权体系特异因服务场景相同如下单以同一BIZ A表达此刻API 和BIZ N:1 两个服务场景一个叫新增如新建用户一个叫更新如更新用户资料两个场景的流程、逻辑截然不同故为两个BIZ。应用服务层面上游调用方希望”若有则更新、若无则创建“的语义故而是一个API假设上游强势且不愿自行编排。此刻API和BIZ 1:N 前两类同时发生则为N:M关系。 所以个人观点是api层并不冗余虽然常见较薄依然尤其独立的职责。譬如特殊业务的定制API、同一业务不同渠道PC、APP不同API、同一业务权限控制力度不同的API。 能否跨层调用dao可以
2016年在做集团结算的重构设计时个人的理念是四层应有严格的次第顺序上层只可见到直接下层跨层是万万不可的否则就讲不清楚架构规范了否则就会导致调用关系混乱否则本该沉淀在domain的逻辑因biz可直连dao而错放了位置等等担心。 近几年反问“为什么一定不能跨层调用”觉得就算跨层调用四层还是那四层只要每层的职责定位清晰。反之纵使跨层调用被禁止落地上依然会从分层合理性的切入点腐化架构。 实践上看”禁止跨层“会导致很多变形的动作出来。 举例query and do something依据A的存在与否做后续逻辑的判断这是biz层里十分常见的片段。而query往往仅是一个daoA里最简单的sql无复杂逻辑无业务属性。在”禁止跨层“禁令下则只能在domain里新添类似proxy的服务供biz层使用这是所谓的”变形“不仅大大增加了程序员的无效代码量更使domain里充斥了无关代码至于什么是应该放domain里的有关代码下文在”领域设计“里会涉及。 此例仅是biz跨到dao的问题若推想至api里需以查询daoB做鉴权之类的判断依据时则更要凭空专造一个biz场景出来那就越发的怪异了。 因此个人会选择放开跨层调用。本质还是四层的职责定位要清晰过程中的架构维护要遇歪立扶是要靠人和机制的。 代码该放哪层厚domain薄biz
首先问题大致出现在biz和domain这两层因为简言之api和dao这一顶一底是不该带业务逻辑的故纠结点集中在中间这两层。当然我也见过把api层业务化甚至顶替了biz层的也见过dao层业务化之下冗长难懂的sql但都是极少数。 然后定义下什么叫个人认为的“复杂业务逻辑”。 本文起头就提到“至少面向5类行业若干业务线且业态差异很大”在此之下是流程、逻辑分支、模型、业务个性化的规模性爆炸各业务线交织在庞大的逻辑空间中从业务角度看很难看清单一业务在此空间中占据的全貌从空间的组成单元看很难看清多少个业务依托其上牵一发而动全身这个叫做“杂”非沉心梳理而不得全。 局部逻辑上如周期订购类业务在订退升降下的剩余价值计算营销类业务的优惠价格计算结算类业务的账期账龄切账调账票款匹配其本身呈现在PRD里就很复杂这个叫做烧脑“烧的是思维逻辑的缜密性。 回到正题这些复杂业务逻辑在biz和domain里摆放的姿势一句话讲就是”biz编排domain service“实现业务表达。 细讲开去有2个点 从系统架构内观的角度看必须要先有domain再有bizdomain承载了该应用最核心的业务能力好的架构里依domain代码逐个阅读便能鸟瞰该应用的核心能力厚domain薄bizdomain要敦实、包容、开放 biz是面向场景的核心是复用下面的domain搭建出一个业务场景讲究灵活。biz是面向场景的一个动词视角domain是面向一簇模型的名词视角场景必发散模型需收敛动词表灵动名词代沉稳这是核心的差异。所以如前文之例切勿出现common biz这类在biz层做大复用的事情。
倘若两个场景间有同性的且不归属于domain、util、infra范畴的个人观点是宁肯做一定的代码冗余。biz管好自己是第一位妄求复用反倒复杂化了。 能否跨域调用dao可以
首先解释下问题即dao何来跨域一说由于domain是从模型间关联度的角度切割而来的一簇紧密相关的模型确定一个domain反过来说即一个模型归属于某个domain。而dao又是对模型存储操作的表达推论出dao亦归属于某个域故而domain调用dao时必有是否跨域一说。 我几次听过“不可跨域访问dao”的说法dao只允许被所属的domain访问跨域则生乱如同跨层一般乃大忌。这个论调很容易想当然的被理解、认可和接受。然遇事之后细想之下便会反思。 举个例子结算系统里会有费用单、对账单、客商这些概念和域。对账单生成这个领域服务里需要访问客商db表check存在性简单的pk query sql变更上一节点模型的状态此处是费用单。 这类跨域的操作可以在biz层做编排如custom.isExist(), reconBill.create(), feeBill.updateState()是可以实现的。 但是个人观点里检查客户存在性、生成对账单、变更费用单状态这三个动作本就是“对账单生成领域服务”不可或缺的组成因在一个领域服务里完整表达而不仅仅是对账单dao insert的封装。而biz层所应承担的”编排“需在更大颗粒度上譬如”生成对账单“ 并 ”生成应收发票“。 所以在“厚domain薄biz”的理念下domain势必要对”隔壁域的存储对象做些简单的无业务逻辑的操作。 澄清一点不论是《跨层调用》里我举例的biz层下跳dao还是此处domain的横跨dao都仅限于“简单操作”。倘若是复杂操作如涉及状态图控制、值枚举控制、值映射控制、对象状态变更的对外通知等业务层逻辑转换之后才能做的存储操作是必须收口到对应的domain的。而至于简单 or 复杂的明确定义说实话我是定义不出来的但又不想因噎废食所以我站在了“厚domain薄biz”架构原则这一面而对dao合理调用控制这一面就得靠执行中的架构把控了。 再补充一个观点上文举例“service间网状调用”使得下层呈现盘根错节的形态不好所以要按域垂直分桶隔离。而此处dao不用按域分桶隔离是因为dao之间是不会横向互调的它是非常独立的个体。某种角度讲dao像infra层仅是锤子、起子、钻头这样的工具功能单一人皆可用只要保证使用者domain层守规矩dao作为工具可尽量灵活。 事务放哪一层视所在业务特性而定同一应用内需持一套标准
这个问题核心还是biz层和domain层的选择把事务控制放在api顶层或是dao底层我是未曾见过的。 讨论前提是我在架构设计里一个biz层的函数调用仅限于一个rpc同步调用的内存函数流而非异步工作流。在此前提下倘若biz和domain是1:1的情况也是大多数情况事务控制在上还是下区别不大因为biz本就很薄。 倘若是1:n的情况编排若干个domain服务放biz则有大事务的性能风险放domain则有一致性风险需视所在行业特性决定了。 我近几年都在财务信息化研发性能挑战相对小些所以从架构的简约性考虑留在biz层做事务控制。倘使有极个别协调多域成为大事务而RT不可接受的把长流程biz拆散并以事务内消息本地DB进行串联当然这会牺牲biz在语义上的完整性个案下是个妥协性方案。 譬如交易需要编排商品、库存、营销、履约等完成一个下单且都是rpc外部调用即使是一个同步调用的内存函数流也没法在biz层做事务控制只能下放到单域服务内部并以重试补偿等机制保证一致性。 同一个应用内部须是同一标准忽而在biz层控制事务忽而在domain层控制事务不可。
2.2 细项2 - 领域
领域设计最核心的是划分切的位置、切出来的大小极大的决定了一个架构的生命长度。 个人在方法论上最核心就两步走 画全局模型图确定网状交错的关联关系找出耦合度最高的一簇圈为一个域从语义的角度确定每个域的职责和服务。 按场景逐个画泳道图推演调用关系检查调用的合理性、域定义的完整度和内聚度。
当然从教科理论上讲最前面还欠缺了一步“按user case 鲁棒图推演出核心模型”。略过这一步是在实操层面上这种系统性的架构设计几乎全都发生在一个业务系统已经发展了若干年而非成立之初。对应研发同学凭经验感觉就能罗列出核心模型且当时架构的核心矛盾不在模型完整度而在于分层分域的问题。所以这是实操上的变通简化。 ✪ 2.2.1 一个反面案例
上面以模型驱动的领域切分方式看起来似乎顺理成章本该如此理所当然的事情似乎没那么复杂。这里给一个2017年自己经历的案例走野路没摸到道前会有纠结点的。 背景基于资金平台系统可以理解为一个资金类的营销系统如红包、储值卡、积分等等营销的业务本来就逻辑复杂系统也因业务拓展的速度而有腐化还有收并的系统亟待整合故而打算做2.0的架构升级。 这是对当时架构形态一个极简版的表达舍去了预算等其它域聚焦在对账户操作这个点上便于表达案例的关键点。 红包、储值卡、积分这些金本位的营销工具的表达且属性各不相同。例如红包是平台or商家直发给c用户的积分是随单购买返给c用户的储值卡是用户自行开通并充值的。但在随单消耗时又是相通的按序或等比例作用于订单抵扣原应付金额 发放、充值、消耗、退回这些场景是各个营销工具都可能涉及到的 有系统整合的诉求如xx积分系统。
谬解一按产品划域 这是想到的一种解法考虑各工具的差异性就划分为独立的域了。当时这种想法有个潜在的思维导向是产品架构设计和产品表达上就是这么归类的。 这种方案下问题有 几种资金工具虽有差异然亦有极大的相同点如随单消耗且不能排除差异变相同例如在发展后期红包亦可充值分开表达会有很大的逻辑重复 不稳定。红包、储值卡、积分本质是产品层面的语义随时可能发展出新的类别譬如会员卡即有储蓄功能、亦有会员优惠功能、还有充值送功能也可能将现有的形态做整合 性能问题如下单抵扣这个链路双十一时biz的随单抵扣消耗场景编排底下各域的领域服务进行消耗性能上走不通。 谬解二按场景划域 这是想到的另一种解法这里思考的突破是资金工具的分类仅是产品层面的语义不能以此为系统架构设计的要素而其差异性应以逻辑扩展的方式表达。而此处用发放、充值、消耗这些场景来切割域思维逻辑上是认为资金工具的操作估摸也就这几个大类了以此切分能完整涵盖整域的表达。 这种方案下问题有 biz和domain的定位更模糊了譬如红包发放和积分发放其流程和逻辑是不同的故而在biz单独出两个场景。然而这样两个流程上都全然不同的事情就能够在发放domain里归一吗显然会很变扭要么就是发放domain做的很薄逻辑都上浮到biz层要么就是domain内部其实有两套完全独立的逻辑和流程来承载上面两个截然不同的场景biz总之domain和biz的关系会很模糊因为说不清“发放”这两个字到底谁负责。 不稳前面用产品名切domain的问题一样这些动词本质上是一种资金工具使用场景的表达是产品功能层面的范畴随时可能被演化和升级。 正解按模型划域 这是最后采用的方案当然仅是极简图真实远比此复杂还有预算域、支付域等等等等。这里的思考突破是把所有动词、逻辑剥离从模型上看最后剩下的只有一个账户域。“只有一个域”是前
面的思考里一直不愿面对的总觉得这么复杂的一件事情怎么也要找个角度多切几刀多整几个域来分而治之。 然而从理论方法的角度看这里涉及的模型簇就只有一个叫做账户所有复杂度都是操作账户前的逻辑计算例如消耗里一个订单按顺序or比例对各个资金工具抵扣计算、主子单的抵扣方式、某个资金工具抵扣上限控制、用户类型及商户类型和资金工具可用性的匹配度等等等等而这些复杂逻辑所操作的对象都是账户所以就是一个域而已。 那么从图上看可能会有一个疑问”发放“在此处标志为账户域的一个domainService相较于上一例作为”发放域“从biz层视角看有啥区别呢换言之发放、充值、消耗分离为单域表达会有什么问题呢 发放、充值、消耗都是对账户做操作势必会有些共性的地方譬如账户的状态图管控。状态图是带业务属性的若冗余散落在各域必然不好若归拢到一个叫util or infra的地方也不好不应含业务若下沉到dao也不好不应含业务若随机放到其中一个域并提供给其它域使用也不好域间不可互调。这类业务属性本该安置在一个领域内但现在却无处安放本因是把不可分割的事情分开表达了。 ✪ 2.2.2 实践中的细节问题 配置和单据同属一域吗否
这个问题挺魔幻的没在实操中去思考过不会觉得这是个问题。但最近某结算系统的重构、发票系统的重构都有同学涉及到了里面的误区。 举个例子结算系统的费用、对账、应收域里原先都会画进一个配置模型譬如费用单配置表达是费用单从哪个业务来哪类单据、哪类业务细项、聚合维度、账期日切or月切、是否需要抛账、等等等等。 从某种角度说这是个业务模型因为它带了很多业务语义且费用单配置、对账单配置、应收单配置都是独立结构化表达的而非schemaless的kv表达。再往下推演这个模型在大图里往哪里放既然叫费用单配置那么就应该和费用单、费用单明细一起组成一个费用域。逻辑似乎很顺。 然而把费用单配置和费用单挤到一个域带来一些变扭的地方。例如模型关系上费用单配置和费用单是什么关系好像没啥关系费用单里不会有一个字段表达是从哪个配置id来的因为配置只是生成费用单的前置逻辑可以是结构化配置也可以是一段代码跑完就完了和目标对象费用单没直接关联关系。 再譬如聚合根是谁应该是费用单吧 可如果这样那似乎费用单配置并不会依赖费用单而存在吧没有单据也还是可以有配置先存在的。这么说难道有两个聚合根在同一个域再例如从域服务上对上提供的域服务既要有对单据的操作服务也要有对配置的操作服务似乎就有两个核心对象服务了这个和聚合根的疑问是同源的。 本质上配置和单据本身是没关系的它仅是表达对单据操作的一种逻辑可以是代码或者结构化配置但配置不是核心业务模型不属于费用单域。 那么这个配置单模型能否成为一个单据的域看必要性。假使对配置的操作更注重配置本身的信息存储功能没有很复杂的业务逻辑那么就由费用单域直接调用费用单配置dao读取就好了。假使对配置的操作有丰富的控制逻辑那就可以成为一个单独的域。 例如从结算视角看合同也是一种配置承载了账期、收付款方式等信息但从合同管理本身的角度看合同的管理亦是有着一套复杂的控制逻辑则可以成为一个单独的域。此处讨论的不是另外一个合同系统而是客商系统中对于结算要用到的合同片段的信息管理 域太小怎么办没事如果它需要是一个域
很多次会被问到这个问题并发生在不同系统重构的语境下。我首先会反问这能够成为一个域吗 不是底层有个模型依托就可以成为一个域的。我对可成为域的定义是对模型做CURD操作之前需要有些业务属性的逻辑承载否则它仅仅只是一个dao实例。 举例某系统里会有个oplog的模型它会记录操作的单据类型、单据id、操作人、操作时间、操作内容、操作结果会和单据操作同一事务内保存在db表里且会有用户查看以及操作记录的合理性审计。 所以它并不能完全归属到infra它带有一点点的业务属性。然而这个模型在领域划分时怎么都没法融入到一个具体的单据域所以它可能是一个域。但它又不是一个域它只是一个业务对象存储的dao因为在对这个dao做CURD前对此oplog的组装加工并不需要一个单独的域服务负责。应收域在对应收单做操作时顺便就把操作信息组装到了oplog里然后一起保存到dao。应付域在对应付单据做操作时也同样把应付操作相关的语义组装到了oplog里。 所以这么看来这个“对模型做CURD操作之前的业务逻辑承载”并不需要一个单独的域承载而是散落在需要使用到oplog的地方。其实上文提到的“配置模型”也是一样的道理看似是一个哪儿都放置不了只能独立为一个域的业务模型然而却并不需要一个单独的域来承载对其操作的业务逻辑。 举例上文某资金系统里提到过充值并不是一个域只是一个使用动作和场景。但此处要补充一点充值动作下是有个充值单来记录状态和过程信息的。那是否要单拎一个充值域来表达对充值单操作的逻辑表达呢 当时的选择是先不单拎一个域因为当时充值这个动作仅储值卡产品有用到不多的业务逻辑在biz层组装掉并由biz层直接访问chargeBillDao完成记录。若发展到后期充值这个事情变复杂了例如可以合并充值、涉及到对充值单本身的分摊计算等则可再行拎出为域。 所以个人的观点是如果它是一个域那么再小也别和别人硬挤到一起。如果它暂时还构不成一个域那先以dao存在着。 域太大怎么办没事如果它只含一个聚合根
太庞大有两层意思一是模型太多二是代码逻辑太多。 对于模型太多实操层面我没见过因为一个域只能由一个聚合根而聚合根和其附属模型间有个共生死的约定附属不可独自苟存所以一般以聚合根为中心的模型簇不会太大。如果有人说这个域里模型怎么这么多一般来说应该是切的不够细了有几簇模型组同挤一个域了。 对于代码逻辑太多我见过挺多的。尤其是核心域譬如资金系统里面的账户域代码特别多因为最复杂。如果以代码量为一个领域圈的大小边界则一个系统的所有域一个个圈圈陈列在一张大图上呈现的景象往往是一家独大或是双雄争霸最多三国鼎立很少出现诸侯割据的局面。这是正常且常见的不要害怕一个域的代码越写越多只要你确定它底下是单簇模型。 它是聚合根么看独立性
怎么看两个模型是独立的两个聚合根还是归属到一个聚合根 个人的实操经验是需要根据场景推演看它是否有独立被操作、被存在的情况。切记不可以凭感觉一定要找业务场景推演。 举个例子账户和流水看起来是账户是聚合根流水是附属模型因为流水不会独立存在流水的操作都是因账户而起。流水查询类不能算虽然流水查询可能是独立的譬如按用户查出所有近期流水不论哪个账户。 但是展开去讲从DDD里面CQRS command query responsibility segregation角度看复杂部分更聚集在command上所以设计考量上会更聚焦在写操作部分。所以流水是账户的附属模型它的变化是账户金额变化的一个体现它不独立。所以账户是聚合根流水是附属模型这是大多数余额类业务系统共性存在的部分譬如资金、预算、库存系统等。 但这个经验值也有例外场景。譬如财务领域里有个银行流水认领背景是to B大额交易客户常选择银行渠道付款常是手工打款。集团从银行账户看到的收款流水和业务语义的应收单据比对往往金额不一致。可能是客户合并付款、预付款且付款备注格式不一需要财务手工去识别、打标、拆解金额最终核销应收单。这是银行流水认领系统的定位职责。此时流水是独立的它是财务操作的核心目标模型它的操作和账户无关故而是独立的聚合根。 所以我们需要根据场景推导看模型的独立性来判断是否为聚合根。 充血模型不建议
对于域服务内部的实现方式会有两种一种我称之为“充血模型驱动式”一种我称之为“平铺直叙式”。
DDD这本书里一个经典的框架方式为 以模型划分域确定该域的聚合根 以聚合根为核心模型基于此构造若干对模型的操作函数并以此为逻辑载体 模型操作函数负责复杂逻辑的表达并将处理和加工结果体现在模型实例本身的结构改变、段值改变 最后以repository.save(模型)的方式将其持久化至DB。 所以对域服务的实现有些同学会基于该域的聚合根模型做逻辑操作将结果反馈至模型实例最后再转换为存储模型完成持久化。然而实操层面上事情会往杂乱的形态演化代码和模型结构越来越复杂难懂。 2016年在资金平台、2022在预算平台见过这类情况尤其是涉及“余额”类的模型第一感觉是很适合用此种方式表达。但事实情况是资金平台里对红包的金额计算、预算平台里对预算池的操作两者的代码都很复杂难懂需要沉下心来耐心梳理方能得解。而事实上这些代码背后需要表达的逻辑又并没有代码本身表现的那么复杂把本来略复杂的事情搞更复杂了这是实操层面的效果。 个人理解这里核心是两个原因 以模型为中心的逻辑承载。DDD里面的例子都是极其简单的。生产实践上尤其是面对和营销业务沾边的资金操作和复杂树状组织架构匹配的预算类操作都不是一个简单的模型能表达的。模型本身是复杂的并且会随着业务的迭代越发复杂呈现出父子模型、树状关系、多层级嵌套结构、稀疏态模型空间等特征。模型不能像代码那样通过SPI有很好的扩展性不论是共性逻辑还是个性逻辑都要在一个模型上体现各种需求和逻辑的叠加下模型会极限膨胀。而对于某一个域服务对应到充血模型的一个函数方法需要了解其逻辑则必须要先把该模型本身整个全部摸透就算该域服务的逻辑只涉及模型的很小一部分改动但你还是得“沉下心来“看清楚全貌因为你不知道这个逻辑将牵扯多少模型的结构、关系、段值的联动性改变心慌。 以模型为中间载体的两段逻辑的叠加效应。DDD里的例子是简单的所以可以模型.operateA()后repository.save(模型)即可。现实中模型是复杂臃肿的将其转换回存储模型本身的逻辑代码亦会很复杂。于是在业务空间的一段需求逻辑到了实现层面将被切割成三段a.模型本身的理解 b.对模型基于需求翻译的操作逻辑 c.域模型到存储模型的转换。到这里可能会有个疑问c.域模型到存储模型的转换不是一套固定代码解决的吗所以我们只要搞懂了a、b即可。然而事实情况是在互联网应用里往往不是像hibernate那种映射式save而是mybatis那种对存储对象局部修改的实现方式将模型里关于该域服务这一个切面的信息量转换为sql进行持久化。因为性能。所以对每个域服务到c阶段的代码都是不同的还是得老老实实的看全abc才能全面知晓代码逻辑。 所以我个人是偏向于“平铺直叙”的表达方式从业务需求到逻辑计算并直接转换为db存储的指令从人的理解上也更容易就算这里的局部代码是面条式也比所谓的“充血模型”的高级设计模型更好理解这是个人体感上面的表达。 从理论上面讲两个点 平铺直叙的方式仅针对该域服务函数的逻辑表达。典型的是query出存储对象的片段、逻辑计算加工、更新回存储对象。注意这里的片段仅针对当前函数不会像充血模型式每次都捞全了。也就是一码归一码一事一议。 互联网的复杂业务逻辑下更注重的是逻辑的承载和表达。在多态业务的冲击下流程逻辑上的代码表达更有利于以SPI的方式将个性化和共性化解耦这比模型为承载体更有优势。 域间可以互调吗绝对不行
这个问题显而易见似乎都不应该是一个问题。其实我想强调的是包结构的设计对“域间不可见”这一原则的落实性影响。 左边这种对领域包的组织方式我见过不好。建议采用右边这种从根上隔开表达的是领域和领域之间是完全独立的就算它们内部的包分层组织类同也请不要按分层的包名合并。隔离方式可以是module也可以是package视系统大小但别混用。 域内聚到什么程度考虑ROI
这个问题的出发点是曾经有同学觉得既然领域要独立。那么领域内部也要分API、biz、core、model、dao几层因为领域们要像独立应用那样虽然现在挤在一个应用里但只要它们间解耦随着业务增长和应用的庞大我们随时可以将它们单拎成独立的应用。 那么领域内部也要做到尽量的内聚通过倒置依赖的方式见下面《倒置依赖》有具体图例将外界的一切实现层面的概念都屏蔽掉让自己像个周身遍布各种型号插口的充电宝一样只关注和知晓自己的内核不感知不在乎将和哪个哪类型号的手机集成。 这个问题放最后阐述因要再次强调。一般教科书上讲的“领域”更多的是类似交易、营销、商品之于电商售卖平台的L1层面的领域它们有独立的应用群、产研团队。而本文通篇提及的“领域”仅限于L1域内的子域乃至单个应用内部的域它们共处于一个应用群、一个产研团队。“划分”这个词背后本质是化整为零、化繁为简在庞大的研发协同中寻求最大的效率。而在面对的是单一小组内部的协同问题时“解耦”的效率收益则需要和“划分”付出的代价做权衡从实操上做一定的耦合性妥协未尝不可。
2.3 细项3 - 开放性 上图借用自集团某同事的技术分享文档非本人原创。 多态业务必有共性和个性理想世界里希望有一个平台将共性部分集中支撑且同时能保持个性部分的灵动调整。现实世界里平台集中复用和业务自主灵动呈反向相关。实现层面无非有四种 平台中心保姆式 平台托管SPI开放式 平台组件化被集成式 行业烟囱自研式。 A、D为两个方向的极端一个极度集中复用但行业自主性极差一个行业极度自由但无共享复用。B、C是兼顾平台集中复用和行业自主性的开放化架构方案B以TMF2.0架构为代表C在“大前台小中台”技术大方向下的一种全新的行业和平台架构关系。 本文话题聚焦在一个应用内部对多态业务兼容性的架构表述不涉及行业和平台关系的论证。然而不论是烟囱式、中心式、还是SPI开放式被多业态业务冲击的系统能做到“系统能力和业务个性化解耦”“业务间解耦”总是好的。 就算整套系统就是同一方人员研发研发内部能把业务定制代码和系统能力代码隔离开来总是好的。就算是行业研发也不表示面对的业务就单一了就不复杂了这是误区。 2018~2020我在集团研发所谓的平台。2020至今我在本地生活研发所谓的前台行业。然而到了前台才发现前台之前还有前台我需要面对饿了么的餐饮、新零售、物流、城代、企餐、口碑等行业的财务需求支撑。 当然在饿了么不会有所谓的行业财务研发来和我协同所有的饿了么财务需求我需要全部支撑即是平台中心保姆式。其实在人员配比合适的前提下这种生产关系非常高效但这并不阻碍我很需要在架构和代码层面将行业特性和系统能力区分开来的自我期望。 ✪ 2.3.1 实践中的细节问题 定制点开哪层领域层
无非是在biz和domain层选择api和dao里开定制点我是没见过的。
按照我”厚domain薄biz“的架构思想肯定会选择domain里开定制点。并且从biz定位上讲希望能灵动针对一类场景即可不奢求复用。倘若要开定制点必然是在”有复用有个异“的基础上逻辑上讲就和biz层不求复用的宗旨矛盾了。所以必然是在domain身上动刀。 反面案例讲原来在结算经历过见上文描述《biz层臃肿service层单薄》这一节。原来老结算系统的架构思想下上层做编排甚至有工作流式的非同步调用编排下层提供service被编排。而下层的service没有扩展点不可被定制语义和内容实现上很明确一就是一且仅只能是一明明确确。 我个人喜欢称这类底层能力叫”实心砖“。一旦这个”实心砖“的内部实现不匹配新业务形态了就算砖头的形态、大小还是可以用的只是局部不匹配就得升级它。如果升级本身和其它业务冲突则得再造一块砖。而架构上觉得这个再造同类砖是不合适的是腐化那就得拜托上层转换成完全匹配该砖的契合要求。 于是一点点的业务差异逻辑逐渐往上浮直至biz层开始臃肿后。通用biz类和它的业务定制点就出现了。再往后发展biz层越发的肿胀底层service层越发的萎缩这是一个逐渐且必然的发展趋势。 而在domain里开定制点我个人喜好称这类底层能力叫”空心砖”砖还是那块砖大小、形状都没变依然能契合原先的继承点只是局部材质、重量、柔韧度上可通过往空心里加不同材料实现。 定制点开多大不要太小
这个问题很难有个概况性的词汇来解答本身就是个设计的”颗粒度“问题”度“这个词本身就是一种类似”火候“这样的经验值只能依菜而定。定制点开很大达到几乎把所在的域服务挖空了所有逻辑均在一个定制点里表达掉了这个我是没见过的估计也不会有。所以问题的核心还是”定制点要开多小 举个例子
2018年结算完成架构升级后按组织要求需要做商业能力透出。按照当时的要求由于要在XH平台对商业能力做显性化表达包括流程、扩展点乃至可以直接在XH运营平台配置一个商业能力的业务规则所以要求扩展点需细细到等同于一个配置项。 以结算记账看扩展点要细到”账期日““账期周期类型-月、周、日“”账单类型-仅记账、直接结算、周期结算“”扣款渠道-CAE、网商、资金账户“具体收入账户id等等这些颗粒度正好和运营配置界面的配置项颗粒度一样因此一个扩展点的实现逻辑直接对应到配置项值。 然而我个人是不认同这么细颗粒度的扩展性的。因为扩展点本质上是一种业务个性化逻辑的表达不论是代码表达还是配置表达甚至是一个proxy代码callback外部服务获得逻辑而这段逻辑若是太碎一方面不好管理一方面不直观一方面站在实现扩展点的研发同学的视角就更是为难了。 站在SPI实现者的视角本来平台对其就是个黑盒面对一堆只见其名不见其实的SPI要把业务逻辑精准的摆放到位实为其难更不说一堆数量庞大口径更细的SPI了。当然站在当时设计者的角度是希望更细的颗粒度让SPI的语义更精确。然我个人的看法是SPI不是一个独立的个体譬如上文提到的”账期日“字面上确实更清晰和精确但背后的隐忧是我对这个SPI的实现会联动这个平台发生什么样的变化我是不清楚的。 就像一个开关只有on off两个选择语义上非常的精确但你按下去之后是关一盏灯还是关一排灯我是不知道的。心慌。 所以我个人的实践经验是SPI代表一段逻辑可以以代码表达的逻辑之后才是两种实现方式 一段业务定制代码 一段系统默认实现代码并读取业务配置获得定制逻辑。方式1、2是并存的根据业务code路由实现方式。以代码逻辑打底的SPI口径不会太小。 业务身份定义没标准能横竖切开业务逻辑分而治之的就挺好。
这个地方我不用专业术语去定义一个什么叫业务因为没法有标准方法定义。同样一个组织看的视角不同认出来的结果也是不同的。 举个例子财务研发线有很多对财务岗位的业务线定义因为财务系统要支持他们的诉求必须在某个维度将其分而治之找出异同。然而站在业务平台尤其是交易、支付、营销这些toC的系统它们会怎么看待财务岗位是一个业务么按照我依稀记得的2019年之前XH架构对业务的定义是必须有独立own商品且有自己的商业KPI的才叫业务。 大约2016年起业务平台搞TMF架构提出平台和业务隔离、业务和业务隔离、业务有业务身份的概念我是极度认同的。尤其是有纵向业务和横向业务的区别尤其是主架构师经常时常常常拿口碑的例子普及他的架构思想口碑是横向淘宝是纵向因为口碑没有自有的商品只是一个售卖渠道我是极以为是的。这么横竖的切分一些问题迎刃而解。 举例在做资金平台架构设计时当时对于红包、储值卡、积分等产品的差异性怎么表达很是困惑。 你说它们不是身份吧但他们确实在同一个域服务里有极大的逻辑差异不用bizCode怎么把这个差异分支路由开呢难不成对红包的逻辑定制每个业务插件实现包里都写一份 你说它是业务身份吧可是和淘宝、天猫、飞猪、盒马这些正牌业务身份是啥关系难不成要二元叠加为淘系-红包、飞猪-红包、盒马-储值卡。好像也挺好记得当初真有往这个方向去想过因为从产品形态上讲业务和产品类别间是一个稀疏矩阵关系。例如集团淘系共用一类红包XX业务红包是独立的YY业务强调储值卡弱化红包。可是到后来业务要打通XX业务的红包集团也能用但规则不同于是出现了XX业务红包在XX业务下是规则A例如使用上限之类的在淘宝下是规则B。到这里就玩不转了。 而今回看不论这些产品类型的差异叫做是横向业务差异也好还是产品差异也好TMF里也有横向产品一说本质就同系统自身能力中有A、B、C三种模式来实现流程的同一节点然后不同业务各自选择适合自己的模式执行该节点。只是红包、储值卡更像是若干个节点的实现模式的一个大集合或者叫做一个实现套餐也是很合适的。它们本质并不是一个业务。 前面说口碑这个经典案例引出了纵横业务的概念。这是站在交易的视角以货权有无来定义一个业务而当时以交易为首的交易链路视角几乎代表了整个业务平台的视角。 然而到了结算的视角从资金往来、发票税务单元、财务核算单元、乃至经营分析单元进行业务的切分此刻虽无货权的口碑堂堂正正的成为了一个独立的垂直业务。所以我说业务身份如何定义因域而异
2.4 细项4 - 解耦
上面是一张画在2018年12月27日的图强调如果一个系统在各方面都能做到解耦其灵活性一定是高级的。这图到现在看基本原则也没变就直接拿来贴上了。 补充几个点 1.不是切的越碎越好
例如层次越多、领域越多、应用越多矫枉过正了。解耦的本质是把事情分而治之但解耦需要付出代价收益和代价之间需根据所在业务特色、研发团队状况做权衡。当然这是句正确的废话总之没有标准思考事物的本质很重要。 见过一些具体的“矫枉过正”的案例。譬如图例在调度器应用和执行器应用之间加了个消息队列做指令的传递注意背景是这两个应用群归属同一研发小组同一业务域。为何不以rpc同步调用方式直接传达指令两者的qps、机器数量、稳定性保障各方面看解耦性两种架构模式差异不大。 这是一个具体的案例。再譬如应用切太细的看似很解耦实则因全局调用链路的复杂化而付出了更大的代价。 2.“语义”的独立性是关键
业务空间有业务空间的语义到了技术架构空间就应该做转换、收敛。譬如预算熔断判断和预算余额查询两类业务需求到了技术空间可以是同一个应用服务对接。应用间也有各自的语义譬如2017年在资金平台时支付应用层面的资金组成标xyz到了资金系统内部就应该转换为红包等资金工具实例的出资来源组成语义当时却把这个xyz的外部语义一直一直深入传递到底层并在各层以此外部语义做逻辑判断依据这是资金1.0系统代码晦涩的一个方面。 分层间也有各自的语义在domain层叫做cancel的一个服务到了dao层就应该理解为update(statue-1)然而我见过上下层语义保持一个频道的譬如biz、domain里的service有叫update的函数看起来是做了一个很通用的函数然而违背了本该在业务层保持语义精准性的原则。 这里扯开去一点做业务研发在实操中我对关键概念名词的精准度比较较真。不论是在研发内部的系分设计还是对PRD的评审都会在意一个概念的取名是否精准。因为不论是代码、还是业务逻辑的传承都和它紧密挂钩。如果一个关键概念取名有二义性很容易造成在协同上鸡同鸭讲造成巨大的潜在的协同成本。 往大了讲一个业务系统架构的腐化往往是从这些含糊其义的类名、方法名、变量名萌芽的。取名不精准也反映出研发同学对业务本质的思考理解不够。 3.倒置依赖
一刀两段切开变为调用方和服务方。站在调用方的角度倒置依赖讲的是“我作为A需要有服务D1供我使用”D1这个交互协议是站在调用者需求角度提出的是个体需求的表达。非倒置依赖讲的是“我作为A看到有服务D适合我使用”D是站在服务方本身具备的角度描述的是公用资源的表达。 落地上有两种实现方式 第一种站在领域层级看倒置依赖譬如域A要D1服务域B要D2服务而对应服务只有D因此D1-D, D2-D是需要分别转换的且D1、D2这个定义的交互协议仅能在领域内部复用作为防腐层的转换代码也应在领域内部
第二种站在应用的高度看倒置依赖应用说我要有一个D服务放在infra层以供所有领域使用因此A、B两个域可以共用D服务不用做转换甚至可以把服务协议D耦合进A、B的领域代码。而在应用层面可以有D的不同实现譬如针对不同表结构的dao实现、针对外部服务的rpc调用实现可以起到对环境解耦的倒置依赖。 本质上讲这是两种内聚颗粒度的问题。颗粒度越小成本越高。个人会选择应用角度的倒置依赖。 第一教科书中“领域”的概念很多是L1的譬如交易、营销、商品这些站在销售电商平台角度讲的域应用和团队都是要划开的。而本文的架构设计围绕的是单域内的架构设计可以理解为单应用内部的子域。 第二因为是单应用内部的故而它并非真正需要那么的独立。而D1、D2这些语义的倒置依赖实现成本是巨大的且大多数应用内划出来的域都是不大的ROI上就要权衡了。故而个人会选择在领域语言独立性基础上做一定的耦合妥协譬如此例中领域感知到infra层的协议语义D。 4.业务模型和存储模型解耦 一般来说新建的系统应用和DB都是完全掌控的都是自家的东西就没那么容易分清楚彼此。发展到后期因为系统和团队的归并需要和异构系统做整合此时前期的业务模型和存储模型是否解耦就很关键了。 举例资金平台发展到后来需要收口集团原散落在行业的资金营销工具例如XX业务积分。此时形态在domain层都是积分模型到了dao层一边是自行设计的db存储模型一边是原XX业务积分的db存储模型。存储层面更有甚至是http调用外部服务当时新零售战略下投资了些超市它们有自己的会员储值卡系统需要系统集成的方式对接使用。 假设前期设计的好只是在dao层有3种不同的实现适配核心的领域代码和模型是不用动的。 推而广之按照DDD六边形法则系统对外的触点理论上都可以接口实现的方式做环境的解耦当然实现上也要考虑ROI。最糟糕的是例如上面讲的xyz标一样一个外部环境的语义在整个应用中从头贯到尾当对接的支付渠道作为外界环境发生变更时就只能做贯穿全身的大手术了。 5.配置态和运行态解耦 主要讲的是“配置”这个东西读用的地方和写给的地方是两件事情。如同上文《领域设计》里提到过的案例客商里面费用单配置它在帮助费用单表达其生成逻辑时这叫“运行态”此时单据是主配置是辅。 而在通过运营平台之类做配置管理时这个叫“配置态”此时配置是主模型它可以成为一个域如果足够复杂的话譬如对配置的结构管理、配置实例的增减、多角色的编辑审批等协同。 三、多系统间架构形态反面
多系统的组合空间巨大没法有个标准形态。这里以个人认为的反面案例反向表达实践的总结。
3.1 微服务切应用
案例是15年共事过某XX域当时该BU的整体架构原则就是微服务化切的真是碎一个挺小的没几个类的原子的功能就可以成为一个微服务应用。 切的碎有它的设计考量springboot让应用的建立成本大大降低docker亦大大压缩了运维成本从团队协同上讲把事情分的更细能尽量规避巨大的沟通成本好处多多。 但我个人是不赞同切的那么碎的。我认同SOA但不认同微服务两者的区别是颗粒度问题。我认同十多年前的某项目把一个巨石系统的淘宝按域切分成交易、营销、商品、结算等等因为每个域背后是一支单独的研发小组。 当组织随着业务庞大后倘若继续守着原来的一个系统一套svn当时行这个是协同上的一种巨大灾难。 但微服务颗粒度不同已然是一个研发小组内部的事还要将应用拆分到每人拿若干个应用听说在原来AE的架构里这里的“若干”是比较大的一个数我不太能理解这里的好处可能是有。 但相较于应用拆分导致的长链路性能、长链路下问题排查效率、每次基础组件升级都要配合着落实N个应用潜藏的运维成本、以及潜藏的应用交接成本、潜藏的对N个应用逐个盘摸得到全局的理解成本远大于收益。
3.2 按层切应用 案例16年做的重构设计设计了5个分层网关层gatewaynotify接收订单完结消息并DB里缓存之之后异步计费结算流程层business沿用bpm编排底层能力等同于标准分层架构的biz层ability层提供底层的原子服务能力供上层编排domain层基于某个模型的操作本质上ability层domain层 标准分层架构的domain层而划分成这两层其实是反面的案例实操层面就会觉得逻辑都在ablitydomain几乎弱化为dao且ability用动词划分有不稳定性问题当然这是上文“单系统内分层架构形态”范畴的一个反面案例了此处不展开。 回到应用划分的问题当初受了层间腐化的苦该沉的能力代码没沉该浮的业务个性化在底下做判断心有戚戚念着重构最大的目标就是要把业务和能力分层开。又担忧现在的设计不被未来人所遵守干错一不做二不休直接把business, ability, domain,dao切开成单独的应用且严格禁止跨层访问寄期望于应用的隔离从架构上把腐化问题给杜绝了。 但又碍于事务问题ability到domain这层一个原子ability服务会跨多个domain修改domain的应用独立将引出跨应用调用的分布式事务问题实难解决。 为了坚持应用的独立曾经考虑过ability到domain走二阶段事务框架参考蚂蚁的xts分布式框架当年很行这个面试都会经常问二阶段问题近几年不知为何这类问题忽然淡出了然又碍于二阶段事务框架对domain的改造侵入之剧烈需要domain每个方法都展开为prepare, commit, rollback三阶段且需根据服务场景用不同的写法写错更容易引故障对研发要求很高最终放弃。 最终的最终采取了折中方案把ability以下三层打包到一个应用里规避事务问题。虽然是折中方案依然横切出了3个应用hjgateway, hjbusiness, hjability。 直到2018左右支援某海外业务项目因为海外部署的成本要求极高故而只能合并到一个应用做海外部署了并借此机会把国内的应用也合并了。合并后内部还是以不同模块做隔离丝毫没有因此增加层间污染的风险。 3.3 按领域切应用 上图是2022年接手的一个预算系统的架构考虑数据安全画的比较简单虽然其承接的业务效果很好领域发展也不错但在系统设计和领域划分上是有些问题的。 我理解当时团队是随着预算发展对业务了解的深入做了架构演进式的领域划分从对业务的认知经验和感觉出发划成了动名结合体。分配、消耗、熔断是动词账户、预算是名词按这些亦动亦名的关键概念划了单个的域并基于此单拎为系统db也是独立的。这是第一点领域划分方法论的问题。 全局设计上出发点上希望对这些应用有个分层按照预算从生产到消耗的时序从上到下划为了分配域、消耗域、管控域。然而事实情况是这些动名词组成的应用间呈现出上下左右互通的全网状调用态。譬如从时序上分配在上、消耗在中、管控在下然后管控通知业务系统熔断活动前是需要查询预算、账户的状态、余额的而这两个名词却是归属在上层的分配、消耗域系统中导致了下层调用上层。这是第二点层次划分角度的问题。 按域单拎应用带来的巨大开支。譬如分配和账户这在模型层面是两个模型簇但并不代表它们就必须是两个应用。 一次预算分配操作要经历 1、建立新预算模型 2、将父预算下的账户余额转出 3、新预算下新建账户并接受转入的余额极简流程大致如此。分割为两个应用势必涉及两个应用间的调用和数据一致性问题而这本可是一个应用里一个biz场景同一事务下对两个domain服务的编排。这是第三点应用切分粒度的问题。 当前预算小组正在做重构的设计目标是解决几个问题 应用太散计划会把图中的预算运营台、分配、账户、熔断四个应用合一起把两个异步job合一起至于消耗未合入主要是考虑这本质上是一个对消息做准实时异步聚合的事情并非一个在线业务服务甚至未来是可以基于实时计算技术栈承载的 在合并后的应用内部做分层、分域的划分 把微观设计上过于复杂导致代码晦涩难懂的点用更符合本质简洁易懂的设计方式再实现一遍。当然这是另外一个维度的话题不展开了。