KISS原则在订单装运模型中的应用
导读:Keep it Simple and Stupid 是软件工程师挂在嘴边的一句话,然而如何才能做到 KISS 原则,却是众说纷纭。本文作者以订单装运与订单支付为例,展示没有充分理解业务复杂性的 Keep it Simple 与实际可以工作的模型之间可能存在多远的距离,适合广大工程师阅读。
作者简介:杨捷锋,曾就职于南开戈德集团、普天集团等公司。作为独立技术顾问曾为海尔集团、沈阳飞机工业集团、上广电NEC、天马微电子等企业提供软件开发与技术咨询服务。目前在一家创业公司担任技术团队负责人。有大型企业应用软件的分析建模、大型开发框架(ORM、IoC 等)的架构经验,多年一直未脱离技术一线的编码工作,近年自认为对系统分析、数据建模、领域驱动设计、项目管理略有心得。
在开发软件的时候,我们经常听到长者传授人生经验:
“避免过度设计!”
“保持简单,KISS 你懂不懂啊?”(Keep it simple stupid,即所谓 KISS 原则啦。)
……
敏捷宣言也说了:
- 可以工作的软件胜过面面俱到的文档
- 响应变化高于遵循计划
So,还等嘛?你们赶紧开始编码吧!
“老大,编什么码啊?这次要做什么我还没搞清楚呢。”你说。
哦,忘了说了,这一次,你们要做的是电商项目。甲方爸爸说了,他要的其实很简单,照着某宝某东抄就很 OK。对了,单子已经签了,大单子,你们老板拍着胸脯保证两个星期后交付。
嗯,再简单的系统,做一下数据模型的设计还是要的。
你,作为开发团队的首席,打开某宝 App,进入“我的某宝”页面,看到“我的订单”分成了几类:
- 待付款
- 待发货
- 待收货
- (待)评价
- 退款/售后
嗯,就从这个简单的地方开始,先来设计一下订单相关的实体。
你是这么考虑的:
订单与订单行项
订单的职责,是记录客户订购了什么东西(也就是“产品”,Product)。一个订单(Order)可能有多个行项(Order Item),表示在这个订单中,客户订购了什么东西、多少数量。
比如,张三可能下了两个订单,两个订单的信息如下表所示:
然后,你决定给订单增加几个物流和支付相关的属性。
现在,Order 的属性是这样的:
- OrderId。
- IsPaid。是否已支付。
- IsShipped。是否已发运。
- IsReceipted。是否已收货。
- ……
So easy!一顿操作猛如虎……两个星期后,系统初步开发完毕。
现在,你们对代码的质量有十二分的自信:甲方爸爸用了它,分分钟挑战某宝某东,三年纳斯达克上市、走上人生巅峰。你们拿着系统到甲方公司那里做演示。
看了你们的演示,甲方仓储部门的人提出问题:
客户下的订单,并不总是能够一次性的把订单中的所有东西都发货给客户的。也许因为没有货,也是因为要发货的时候发现仓库里的货已经损坏了。碰到这样的情况,我们会跟客户商量,很多时候,我们是需要先把一部分产品/数量先发给客户的。
听了这个说法,你感觉事情好像有点不妙。你们老板也参加了会议,他对你说:“把这个需求记录下来,马上改。”
这时候,甲方市场部门的人发话:
能不能加个储值功能?我们的客户可以先充值(充值到储值账户),然后用储值账户里的钱消费。一个订单,如果储值账户的钱不够,那么可以部分使用储值账户支付,部分使用支付宝、微信支付支付。对了,做个积分兑换功能也应该很简单吧?每 100 个积分抵 ¥1.00。我们可以设置一个商品,积分最多能够抵扣多少钱这样子。
你们老板说:“这个事情很简单,一两天我们就能搞定。”你的后背开始流汗,你知道这个事情一点都不简单……
难熬的会议终于开完了。会后你找到你的老板,告诉他这些需求做完你们最少需要两个月。你老板听后暴跳如雷,合同只收了首款,他本来还指望你们一个星期内搞定,好跟甲方要剩下的钱。现在你居然说最少还要两个月?你很委屈:“这么多需求,比我们预想的复杂太多,系统几乎要推翻了重做,你找谁来也不可能一两个星期内做完。”
你老板很生气,甚至开始怀疑你在讹他。“谁来也不可能?我给你们找个外脑,给你们参谋参谋,你们一起来做这个事情。”
人脉资源丰富的你老板立马抓起电话,请来了一个“贤者”。
下面进入贤者时间(以下是贤者的话)。让我们先从订单发货说起。
订单与订单装运组
一个订单可以分多次发货,从而形成多个订单装运组(OrderShipGroup)。也就是说,“订单”和“订单装运组”实体是 One to Many 的关系。
订单装运组的 Id,由两部分组成,OrderId 以及 OrderShipGroupSeqId(订单装运组序号)。我们使用订单装运组序号来标识“订单的一次发货”。
就像订单有行项,订单装运组也有自己的行项,我们就叫它 OrderItemShipGroupAssociation 吧。它的 Id 由三部分组成:OrderId、OrderShipGroupSeqId、OrderItemSeqId(订单行项序号)。
像上面张三的两个订单,可能需要这样做发货计划:
对了,装运组存在的意义,还有利于按库存情况进行库存保留、备货、拣货等操作。这些业务流程的信息,有可能记录在 OrderShipGroup 及 OrderItemShipGroupAssociation 实体中。
既然,这两个订单都是张三的,收货地址都一样,为什么我们不一次性发给张三呢?
我们完全可以把多个订单 / 订单装运组的发货需求合并到一个装运单(Shipment),提高作业效率、节约资源。
订单与装运单
一个装运单(Shipment)可以装运多个订单(确切说是多个订单装运组)的货。
So,“订单装运组”和“装运单”是 Many to Many 的关系。当然订单和装运单之间也是 Many to Many。
比如,我们想把上面两个订单装运组的东西,一次性发给客户张三,从而形成了一次装运(Shipment),如下表所示:
如上所述,订单行项与装运单行项之间是一个“多对多”的关系,对于关系模型来说,这需要一个中间的关联实体。那么,这个关联实体怎么设计?
我们是不是可以给 OrderItemShipGroupAssociation 增加两个属性:ShipmentId 以及 ShipmentItemSeqId 属性,让它作为订单行项与装运行项之间的关联实体?
另外,OrderItemShipGroupAssociation(订单行项与装运组关联)的数量,其实是个“计划发货数量”,那么,实际的发货数量又记录在哪里?是否在 OrderItemShipGroupAssociation 里面增加一个“发货数量”属性,用于记录实际发货数量?
这么做,OrderItemShipGroupAssociation 承担的职责是不是太多了?干脆,我们再增加一个实体吧?
订单的项目发货(Item Issuance)
这里我们增加了一个独立的实体,叫 ItemIssuance(项目发货)。
这里假设我们没有为 ItemIssuance 选择使用组合 Id(组合主键),它有一个单列的 Id(主键)。
ItemIssuance 的属性包括:
- OrderId。订单 Id(订单号)。
- OrderShipGroupSeqId。订单装运组序号。
- OrderItemSeqId。订单行项序号。
- ShipmentId。装运单 Id。
- ShipmentItemSeqId。装运单行项序号。
- Quantity。(实际的)发货数量。
上面的前三个属性,用于指向(Referenc)OrderItemShipGroupAssociation。ShipmentId 与 ShipmentItemSeqId,用于指向装运行项。
ItemIssuance 是处于装运单与订单之间关联实体。像这样:
在上表中,忽略了 ItemIssuance 用于指向其他实体的若干属性(OrderId、ShipmentId 等)。
ItemIssuance 没有使用组合主键可以更灵活一些。比如这样,很容易将上一个表格的第一行被拆成两行:
在这里,我们在 ItemIssuance 这个实体上记录更多的实际发货详情,比如产品的序列号(Serial Number,即 SN)。
至此,我们已经构建了订单装运(发货)相关的数据模型。可以认为,订单本身其实是没有“发货状态”的。它的发货状态,是从其他实体(Shipment 等)的状态计算(派生)出来的。当然,从效率角度考虑,有些状态计算出来之后,可以缓存起来。
一个订单可能既处于“待发货”的状态,同时也处于“已发货(待收货)”的状态。聪明的你可能已经看出来了,上面的数据模型实际上是从开源项目 OFBiz 借鉴而来的。
搞定了“物流”的问题,我们再来看看“资金流”的问题?
订单的支付
这一块的数据模型,我们也可以看看 OFBiz 是怎么设计的:
- Invoice 表示“付款请求”。这里不要把 Invoice 单纯理解成我们拿来报销、抵税的那个发票,看看 Invoice 词条的英文解释。
- Invoice 和订单之间是多对多的关系。可以 XxxxBilling(Xxxx 计费)实体,表示 Invoice 和 Order 相关实体之间的多对多关系。比如 OrderItemBilling(订单项计费)、ShipmentItemBilling(装运项计费)。
- Invoice(“请求付款”)并不一定会形成真正的付款(Payment);付款不一定一次就付完;或者,付款可以一次付多个“付款请求”的款项。所以,“付款”本身需要独立的实体(Payment)来表示。PaymentApplication 用于表示实际付款(Payment)和付款请求(Invoice)之间的多对多的关系。
在“简单”的情形下,“订单”和“付款请求(Invoice)”及“Payment”之间,可能是一一对应的关系。
但睁眼多观察一下现实世界,稍微多想一下,就可能意识到,订单、账单、付款,显然有时候它们不是一一对应、严丝合缝的:
- 我(客户)要买啥啥啥(Product)、要买多少,这是 Order(订单)。
- 你把账单给我,要我付款,这是付款请求(Invoice)。
- 付款是 Payment。我先付一部分行不行?我一次付多个账单行不行?一般情况来说,当然可以。收钱不收那不是该吃药么?
订单和支付的关系,对比订单和物流的关系,其实是有相似的地方的。就像订单的物流状态不是订单本身的状态一样, 订单本身没有支付状态。它的支付状态是一个派生的状态,是从其他实体的状态计算(派生)出来的。
如果按照这个模型做,如果说一个订单(Order)“已支付”,那么其实是说:
- 通过相关的“计费”(XxxxBilling)实体,可以找到该订单关联的付款请求(Invoice);
- 所有的这些付款请求存在——通过 PaymentApplication——关联的实际付款(Payment)记录。且 Payment 分配到付款请求(Invoice)的实际付款金额已经等于(或大于)付款请求的“应付金额”。
和订单发货类似,一个订单完全有可能处于一个“未支付”与“已支付”中间的状态——也就是部分支付的状态。
考虑一下组合支付的需求:我们可能需要支持客户使用在我们的电商系统的储值账户的余额进行支付,余额不足的时候,可以使用支付宝或者微信支付不足的部分款项。
我们的电商系统和支付宝、微信支付不可能使用数据库事务来保证“一致性”,所以中间状态的存在不可避免。上面的模型可以很好地支持“最终一致性”的实现。
以上,是贤者的话。
Keep it simple, smart
在项目初期,费心巴力地去构建一个(如贤者所介绍的)“复杂”模型,有什么好处?
回头看看一开始我们的“简单设计”,试图在 Order 中使用几个属性来“搞定一切”:
- IsPaid。是否已支付。
- IsShipped。是否已发运。
- IsReceipted。是否已收货。
- ……
对比后来讨论订单发货业务流程,贤者使用的词语:
词语(英文,代码中使用) 解释(中文)
Order 订单
OrderItem 订单行项
OrderShipGroup 订单装运组
OrderShipGroupSeqId 订单装运组序号
OrderItemShipGroupAssociation 订单行项与装运组关联(可以理解为订单装运组的行项)
Shipment 装运单
ShipmentItem 装运单行项
ShipmentItemSeqId 装运单行项序号
ItemIssuance 项目发货(发货行项)
这还不包括讨论订单支付流程时涉及的概念,Invoice、Billing、Payment、PaymentApplication 等。我们是不是可以一直保持简单?这取决于领域。有时候,领域的现实就是复杂的,如上面讨论的那样。所以贤者的话要不要听,听多少,这个需要你结合领域现实来英明判断。
我们可以摸着良心问问自己:如果领域的现实就是如此复杂,我们是否可以先“简单”地开始编码,然后随着业务发展,被动地响应客户(业务人员)提出的功能需求,开发人员自行其是地编码、“重构”,“演进”出这样一个复杂但仍然清晰、合理、具备良好的概念完整性的模型?
理论上可以,但是我根本没见过这样的事情实际发生过(大概我孤陋寡闻)。不知道什么是复杂,怎么知道如何保持简单?
我见过经过数年“演进”、有 800 多张表的电商系统。维护这样的祖传代码,对开发人员来说简直是一场噩梦。我的评估是:如果以合理的模型实现,大概 300 张表就可以实现同样的功能需求,并且系统会更健壮、更具可扩展性。这样的系统,代码即使能重构,整个模型也应该被预先、精心、重新设计,然后把设计结果放在团队所有人都能看到的地方,作为一个目标(立 flag;),每一步重构都应该朝着这个目标前进。
很多时候,要获得一个良好的模型,并不一定需要被现实打得鼻青脸肿,要做的只是预先学习。
我的个人看法,天底下所有的公司其实都是差不多的。管你是什么 B to b 还是什么 b to C,很多时候,“生意”、买卖本来都是一样做的。以为自己业务很特别的、值得从零开始去“定制开发”软件的,大多数时候,其实只是自己“看得少”和“想得少”。( ̄▽ ̄)"
很多团队声称的 KISS,不过是偷懒的借口。如果项目不死,总有一天团队需要为前面的潦草、Simple 和 Stupid 埋单。
Keep it simple, smart,阅尽世界世间繁华,才有资格说自己真心喜欢坐在旋转***上的简单快乐。
参考阅读:
- 从工具到社区,美图秀秀大规模性能优化实践
- 使用Spring 5实现响应式微服务架构,简洁版来了
- 一百人研发团队的难题:研发管理、绩效考核、组织文化和OKR
- 也许是最简洁版本,一篇文章上手Go语言
- 5G创新应用实践-赋能万物互联的引擎
技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。转载请注明来自高可用架构「ArchNotes」微信公众号及包含以下二维码。