这是一个关于订单的故事。四个月前,我在某刚刚上市不久的网上书城框框网购买了一包纸尿裤,因为尺寸不对,我选择了退货,由此开始了我糟糕的用户体验:首先是快递公司取回了纸尿裤却没有还款给我,接下来,在两个月的时间里,我不得一次又一次的向框框的客服投诉,客服很客气,她让我说出我的订单号然后说需要帮我查一查,两分钟后,她说需要和快递公司联系,稍后再打给我;随后的客服都很客气,但无一例外的,她们都不清楚我订单的处理情况,她们甚至很惊讶,快递公司还没有给你办吗?终于,我失去了耐心,我说,难道你们框框就只负责卖东西吗?送货外包了你们就不需要负责了吗?究竟是谁在处理我的订单,谁是责任人你们不知道吗?整个流程的处理状态你们清楚吗?客服支支吾吾半天,说,我们这就和快递公司联系。
框框的问题出在什么地方?顾客找不到这个订单的直接负责人,只能和客服打电话,而框框自己也失去该订单的状态了,于是只能再打快递公司的客服,这样问题透过组织结构层层传递下来,执行力可想而知。那为什么顾客找不到订单的直接负责人呢?因为顾客失去对自己订单的可视化了,我不知道我的订单现在处于一种什么样的状态,正处于哪一个步骤,谁是这个步骤的责任人,网站上没有,自然无从得知。想想我们的迭代和show
case,也正是一种对顾客的可视化手段。
在下面的章节中,我们将一起来应用rest的架构风格逐步搭建一个端到端的流程管理系统,看看如何解决这个问题,这个问题就是:看在上帝的份上,让我看看我的订单。为什么使用rest?因为在下面的故事中,我们将会有大量的系统集成工作。为什么集成要用rest?因为REST的实质是充分利用HTTP协议,将其作为一种应用协议,而不仅仅是传输协议,这样在WEB上做集成时能够最大程度上的达成一致,形成大家都能接受的约定,减少集成的工作量。另外,不可忽视的一点是,我们更多的是做数据的mashup,这很适合rest的应用特点。不能使用web
service吗?当然可以使用web service,只是我们这里使用rest而已。
第一个需求,我想随时随地查看我的订单
好吧,很自然,这需要我们的程序支持移动设备。
图 1 昨天我们这样开发程序
似乎就在昨天,我们还在如图1一般开发程序,我们一边打开firebug进行调试,一边诅咒IE的不得好死,那时我们的关注点集中在前端,集中在如何使各个浏览器的行为和样式保持一致。而服务器端则是经典的MVC框架,直接将渲染好的HTML文档扔回客户端。
图 2 今天我们这样开发程序
然而到了今天,一切都发生了变化,我们开发的程序成了图2的样子,随着IE向标准的靠拢,HTML5似乎有一统客户端之势,然而,移动互联网的兴起让我们编写程序重新变得复杂,昨天我只需要支持浏览器,现在则需要支持各种手机平台上的原生应用。自然,和昨天存在大量的javascript和css框架来抹平不同浏览器之间的差别一样,现在我们也有了PhoneGap和Titanium来抹平不同平台之间的差别,尽管目前这些跨平台工具还存在用户体验不理想的问题,但最重要的变化来自两个方面:一是客户端重新变胖;二是服务器端由返回渲染完成的页面退化为返回数据,具体表现就是对客户端暴露出API。
对移动设备的支持使得我可以在写代码时、吃饭时、睡觉时甚至坐马桶时随时查看我的订单(当然前提是你得有这些设备),而本文后续的架构变化也会围绕着图2逐渐演进。至于明天HTML5是否会最终代替原生应用,我觉得不会,不仅仅是技术原因(对设备硬件的使用)更重要的是商业原因,替代后的苹果变得和现在的微软/诺基亚一样尴尬,那么,也许后天?
好吧,既然是REST的API设计,我们来看看REST的架构风格。RESTful
架构遵从以下几个原则:
- 请求是客户-服务器 式的,并很自然地使用一种基于拉的交互风格。
- 请求是无状态的。每个从客户端到服务器端的请求都必须包含理解此请求所需的全部信息,而且不能利用服务器上所存储的上下文。
- 客户端和服务器都遵从统一的接口。所有的资源都可通过 Web 的普通接口进行访问 —— HTTP
及 HTTP 方法:GET、POST、PUT 和 DELETE。
- 客户端通过URI与命名的资源进行交互。
- 将http 状态码作为系统的状态码。
REST的实质是充分利用HTTP协议,形成大家都能接受的约定。
看例子,我们以订单列表作为整个应用的调用入口,我们首先会GET:http://api.kuangkuang.com/orders,服务器返回以下的数据:
<orders>
<link rel="list" media-type="application/xml" url="http://api.kuangkuang.com/orders"/>
<order>
<id>1000</id>
<state>draft</state>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
</order>
<order>
<id>1001</id>
<state>completed</state>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1001"/>
</order>
</orders>
在返回的数据中,我们看到了:
<link rel="detail" media-type="application/xml"
url="http://api.kuangkuang.com/order/1001"/>
这个链接引导我们查看具体的订单信息,我们GET:http://api.kuangkuang.com/order/1000,服务器返回以下的数据:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<content>
<id>1000</id>
<state>draft</state>
<cost>88.0</cost>
<link rel="edit" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<link rel="delete" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
</content>
</order>
这里我们看到了两个链接:
<link rel="edit" media-type="application/xml"
url="http://api.kuangkuang.com/order/1000"/>
<link rel="delete" media-type="application/xml"
url="http://api.kuangkuang.com/order/1000"/>
它们告诉我们可以对这个处于草拟状态的订单进行修改和删除。我们GET另外一个已完成的订单看看:http://api.kuangkuang.com/order/1001,返回数据:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1001"/>
<content>
<id>1001</id>
<state>completed</state>
<cost>66.0</cost>
</content>
</order>
没有更多的链接,这意味着我们只能对该订单进行查看。
在这些交互中,最重要的是服务器端返回数据本身已包含了对其他资源访问和对现在资源操作的线索。这样的好处在于客户端只需要一个入口地址,其他所有的操作地址全部由服务器端确定,这使得客户端与服务器端解耦,客户端不必再硬编码入URI,能够各自独立的进化,服务器端负责数据、权限以及交互URI的确定,客户端重新回归展现数据的单一职责。
第二个需求,实现一个简单的流程
在上面的例子里,我们看到了订单的CRUD操作,但这并不是实际生活中的真实情况,整个订单的生命周期如下图所示:
图 3 订单的完整流程
在实现这个流程时,我们分为两步:第一步对订单进行资源建模;第二步通过工作流对订单进行流程的生命周期管理。
图 4 订单的资源模型
图 5 使用工作流管理订单的生命周期
工作流的职责在于管理订单的生命周期,在其生命周期的不同阶段,我们会有不同的参与者,对订单不同的操作权限。我们的系统架构演变成下面的样子:
图 6 使用工作流管理资源模型的生命周期
看例子,这次将我们视角转移到框框网这边,看看框框如何处理我们已提交的订单。我们使用GET:http://api.kuangkuang.com/orders?status=waiting-review来获取所有需要审核的订单,服务器返回以下的数据:
<orders>
<link rel="list" media-type="application/xml" url="http://api.kuangkuang.com/orders"/>
<order>
<id>1000</id>
<state>waiting review</state>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
</order>
</orders>
我们查看具体的订单信息,我们GET:http://api.kuangkuang.com/order/1000,服务器判断出我们是框框网员工,返回以下的数据:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<content>
<id>1000</id>
<cost>88.0</cost>
<state>waiting review</state>
<squence>
<activity rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000"/>
</squence>
</content>
</order>
注意到这两行:
<state>waiting review</state>
<squence>
<activity rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000"/>
</squence>
这段信息是由工作流加入的,它告诉我们当前订单的状态为等待审核以及下一步需要我们来审核。那么,我们PUT
http://api.kuangkuang.com/review/order/1000告诉服务器我们审核通过,服务器返回数据:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<content>
<id>1000</id>
<cost>88.0</cost>
<state>waiting send</state>
<squence>
<activity rel="send" media-type="application/xml" url="http://api.kuangkuang.com/sent/order/1000"/>
</squence>
</content>
</order>
同样,工作流加入了这两行数据:
<state>waiting send</state>
<squence>
<activity rel="send" media-type="application/xml" url="http://api.kuangkuang.com/sent/order/1000"/>
</squence>
告诉我们当前订单状态为等待送货,而下一步需要我们来完成这一步。此时,如果顾客来查看自己的订单会得到什么数据呢?服务器会判断出当前请求的用户是顾客,那么:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<content>
<id>1000</id>
<cost>88.0</cost>
<state>waiting send</state>
</content>
</order>
顾客能够看到自己的订单正处于等待送货状态,而不会有下一步的动作。恩,很好,框框网订单处理速度很快,而这正是网购的主要竞争力之一。
等等,如果订单审核不通过呢,继续看流程:
图 7 增加一个网关
作为框框网的员工,我们GET:http://api.kuangkuang.com/order/1000,服务器判断出我们是框框网员工,返回以下的数据:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<content>
<id>1000</id>
<cost>88.0</cost>
<state>waiting review</state>
<squence>
<xor rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000">
<choice>pass</choice>
<choice>reject</choice>
</xor>
</squence>
</content>
</order>
注意到这两行:
<squence>
<xor rel="review" media-type="application/xml" url="http://api.kuangkuang.com/review/order/1000">
<choice>pass</choice>
<choice>reject</choice>
</xor>
</squence>
系统告诉我们这是一个排他网关,我们需要作出选择,在客户端与服务器端就media-type达成一致的情况下(即客户端能够充分理解服务器端返回的数据格式,这个数据格式被标准化),我们的代码不需要作出任何的修改,pass和reject作为工作流变量被put到服务器,由工作流引擎进行处理,同样不会影响到订单的资源建模。
在这些交互中,最重要的是我们通过工作流实现了REST社区所呼吁的“将超媒体作为应用状态的引擎(hypermedia
as the engine of application state)”。更简单地说,URI代表了状态机里的状态迁移。我们通过<squence>、<activity>和<xor>标签让客户端是通过跟随链接的方式来操作订单状态机的状态转移。
第三个需求,框框将物流部分外包
在我们实际的生活中,电商们并不自己送货,他们将这部分工作外包给了物流公司。从成本的角度考虑,外包送货是最合适的选择。实际上,整个订单从提交到最后的完成情况还要稍微复杂一些,如下图所示:
图 8 订单从提交到完成的整个流程
从图中我们可以看出,这个流程跨越了两家公司,同时也涉及到了三个系统的集成,这三个系统分别是框框网的前台网站、框框网的后台负责仓储、进出货和物流的ERP系统以及外包物流公司的ERP系统。三个系统各自有自己的处理流程,整个订单的端到端处理流程由这三个系统的三个流程所共同完成:当我们在框框网提交订单时,一个消息被发送到框框的后台ERP系统,这个消息触发一个货物的出库流程,当货物打包完毕出库时,一个消息被发送到物流公司的ERP系统,同时触发物流公司的包裹配送流程,当我们给物流公司的配送员付款完毕时,对我们顾客来说框框的购物流程已经结束,然而整个流程依旧还要继续,配送员回到公司完款,一个消息被发送回框框的后台ERP,物流公司的包裹配送流程结束,框框网的这个订单这才处理完成。
在本文的一开始,我们提到了那个糟糕的退货故事,问题就在于当订单交由物流公司进行货物配送时,我们包括框框失去了对配送流程的可视化,物流公司的处理情况在我们的流程中黑盒了。如何解决这部分的问题呢,有两种处理方法:一是在框框网订单处理流程中加入捕获事件,正如图8中所示的,当框框后台ERP和物流公司ERP对订单进行处理时,每到一个任务节点就给框框网的订单处理流程发送消息,由此给我们标示出订单的实时状态。
图 9 通过系统集成传递订单实时处理消息
现在,让我们来看看自己的订单会得到什么数据呢,GET http://api.kuangkuang.com/order/1000框框网前台网站返回数据:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<content>
<id>1000</id>
<cost>88.0</cost>
<state>waiting send</state>
<history>
<activity rel="submit" time="2011-6-28 14:00" participant="ronghao"/>
<activity rel="review" time="2011-6-28 14:30" participant="xinpeng"/>
<activity rel="delivery package" time="2011-6-28 15:00" participant="haorong"/>
<activity rel="warehouse" time="2011-6-28 17:00" participant="pengxin"/>
</history>
</content>
</order>
订单状态为等待物流公司送货,注意到这段数据:
<history>
<activity rel="submit" time="2011-6-28 14:00" participant="ronghao"/>
<activity rel="review" time="2011-6-28 14:30" participant="xinpeng"/>
<activity rel="delivery package" time="2011-6-28 15:00" participant="haorong"/>
<activity rel="warehouse" time="2011-6-28 17:00" participant="pengxin"/>
</history>
工作流加入了订单处理的历史信息,从这段信息可以看出,我们要明天上午才能收到自己的货物了。
很不错不是吗,但是现实情况又是怎样呢。我们先来看看当当,当当如是说:订单状态变为“已发货”后,您可以登录“我的订单”,点击订单号进入订单详情页查看快递公司的联系方式,用订单号查询即可。我们再来看看卓越,卓越如是说:宅急送配送的订单:登录宅急送网站或拨打020-82252310-802查询;港中能达配送的订单:登录港中能达网站或拨打020-86443920查询。也就是说物流公司的配送流程状态并没有集成到网站中来,如下图所示:
图 10 网站与物流公司系统没有集成
为什么没有集成呢?第一是物流公司的客户往往不止框框一家,第二是框框也不会把鸡蛋放在一家物流公司的篮子里,这些给系统集成带来了难度,我们会突然发现有大量的系统需要集成,而系统间的集成之间存在太多的集成点,调试以及约定,这些都需要大量的工作和成本,系统们被紧紧的耦合在一起。
既然第一种实现方式使得我们即时查看我们订单状态成本太大,那我们看看第二种方法:使用一个统一的流程管理系统来管理整个端到端的流程。
图 11 使用统一的业务流程管理系统管理端到端流程
业务流程管理系统的职责有两个:一是由其管理起各个系统间的集成工作,这样避免了各个系统间的大量耦合;二是由其跟踪订单状态,完成订单在整个流程中的可视化。
图 12 框框订单的端到端流程
我们来看看具体的api调用,当我们在框框网站提交一个ID为1000的订单时,框框网站会发送一个消息到http://api.kuangkuang-bpm.com/process-definition/1,由此触发整个的流程,启动一个新的流程实例。发送的消息:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<content>
<id>1000</id>
<cost>88.0</cost>
</content>
</order>
业务流程管理系统给我们返回的消息:
<process-instance>
<link rel="process-instance" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-instance/1"/>
<content>
<id>1</id>
<data>
<order-id>1000</order-id>
<order-cost>88.0</order-cost>
</data>
<definition>
<link rel="process-definition" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-definition/1"/>
</definition>
<current-activity>
<name>订单提交</name>
<link rel="activity-definition" media-type="application/xml" url="http://api.kuangkuang-bpm.com/activity-definition/1"/>
</current-activity>
</content>
</process-instance>
返回的消息中给我们指出了该订单所关联的流程实例ID,当前正在执行的任务。流程系统创建流程实例后接下来继续往下执行,它发送一个消息到框框的后台ERP系统,触发后台ERP系统对订单的处理,同时告诉其访问当前流程实例的URI。现在我们假设流程执行到物流公司的配送任务,我们在框框网站查看订单的即时状态系统会有哪些动作。第一步我们同样是GET:http://api.kuangkuang.com/order/1000,返回的数据:
<order>
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang.com/order/1000"/>
<link rel="process-instance" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-instance/1"/>
<content>
<id>1000</id>
<cost>88.0</cost>
</content>
</order>
返回的消息中多了一个访问流程实例的URI,那么我们的客户端程序继续GET:http://api.kuangkuang-bpm.com/process-instance/1,返回的数据:
<process-instance>
<link rel="process-instance" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-instance/1"/>
<content>
<id>1</id>
<data>
<order-id>1000</order-id>
<order-cost>88.0</order-cost>
</data>
<definition>
<link rel="process-definition" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-definition/1"/>
</definition>
<current-activity>
<name>物流配送</name>
<type>sub-process</type>
<link rel="detail" media-type="application/xml" url="http://api.zjs-erp.com/order/2000"/>
<link rel="activity-definition" media-type="application/xml" url="http://api.kuangkuang-bpm.com/activity-definition/3"/>
</current-activity>
<history>
<activity name="提交订单" type="start" time="2011-6-29 14:00" participant="ronghao"/>
<activity name="仓储出货" type="sub-process" time="2011-6-29 15:30">
<link rel="detail" media-type="application/xml" url="http://api.kuangkuang-erp.com/order/1000"/>
</activity>
</history>
</content>
</process-instance>
我们看到当前正在执行的任务是物流配送,这是一个子流程任务,想具体了解这个子流程执行的情况,我们的客户端程序继续GET:http://api.zjs-erp.com/order/2000,啊哈,框框将配送任务外包给了宅急送啊。
<order>
<link rel="detail" media-type="application/xml" url="http://api.zjs-erp.com/order/2000"/>
<link rel="process-instance" media-type="application/xml" url="http://api.kuangkuang-bpm.com/process-instance/1"/>
<content>
<id>2000</id>
<cost>88.0</cost>
<current-activity>
<name>配送</name>
</current-activity>
<history>
<activity name="接受包裹配送单" time="2011-6-29 15:40" participant="ronghao"/>
<activity name="包裹入库" time="2011-6-29 15:45" participant="xinpeng"/>
</history>
</content>
</order>
好了,有了这三段数据,我们就可以清楚的看到订单所经过的各个环节以及当前的状态。从中可以看到两点:一是我们通过kuangkuang-bpm.com所提供的流程服务将各个系统进行了数据和流程的集成;二是各个被集成的系统需要实现rest的api以供我们的客户端程序进行数据的mashup。
故事完了吗?还没有,京东618活动简报:收获订单40多万份,订购金额超2亿,已经发货一个多亿,尚有十几万份订单积压,大约三日左右可以处理完毕。不足之处:流量多次超过4个G,服务器运行缓慢;图书备货量严重不足。与服务器相比,我更加关心如何及时将这几十万份订单处理完毕以及库存如何应对促销而产生的水平震荡(图书备货量严重不足,无法预测哪些书籍畅销哪些滞销,由此带来的订单迟迟无货),这显然不是一家物流公司可以完成的,需要多家物流公司一起消化这些订单,那么,问题来了,当我们mashup数据时,如何对这些物流公司返回的不同的数据格式进行处理?为每家公司实现一个适配器?NO!作为行业的老大,作为一家一流的企业,我们得制定标准。这时就需要标准化media
type了,建立行业标准,企业级rest等于自定义、创造和标准化media type。
第二个问题,kuangkuang-bpm.com算是云服务否?是的,目前算作框框私有云,对自己和业务伙伴提供流程服务。
最后一个需求,框框要开放平台
最后一个需求有些跑题,但是那帮人现在都在搞什么开放平台,框框自然也是要跟风一下的。那么,开放平台都开放了个啥?
图 13 框框的开放平台
第一是用户的开放,这个通过网站实现,我们增加上百货、品牌频道,吸引商家入驻,这些入驻的商家能够分享我们的网站用户,同时,品牌的用户也能够被吸引到我们的网站上。同时,开心、人人、新浪微博、腾讯微博的账号能够直接登录我们网站,同时,我们网站的产品、评价也能分享到这些SNS网站里。
第二是服务的开放,这个通过流程实现,我们能给商家提供仓储、物流、投诉等一系列的服务。不使用我们的网站,自己有网站?没关系,只要数据格式满足我们定义的media
type就行。
图 14 流程服务的开放
这算完了吗?不算!小马哥说,要开放就全面开放。我们将仓储环节和物流配送环节也开放出来,允许任何服务提供商使用我们的流程服务。
图 15 物流环节的开放
只要系统提供数据格式满足我们定义的media type就可以加入到我们的流程中来,分享我们的客户,为上帝提供服务。我们则从中提成。
所以,真正的开放是整个流程服务各个环节的开放,不仅仅对流程消费者,也对流程服务提供商,我们在其中起一个协调的作用。由此,我们将kuangkuang-bpm.com服务开放出来变成一个公有云服务,后来,我们惊讶的发现,kuangkuang-bpm.com比kuangkuang.com更赚钱,因为它建立起了一个生态链。
小结
在上面的故事里,我们一步步看到了框框的系统架构演进,不难看出,架构演进的过程实际上系统不断分解的过程。
对移动设备的支持,使得我们将页面渲染逻辑从服务器端剥离出来交由客户端完成,服务器只负责提供数据。通过rest的超媒体特性,客户端和服务器端程序能够各自独立演进。
对订单流程的支持,使得我们采用工作流技术,将资源模型(订单)与其生命周期模型分离,分别交由原有系统和工作流系统管理,这样当某部分需求发生变化(例如增加一个审核不通过步骤),能够隔离变化,容易修改。
对端到端跨系统流程的支持,使得我们引入一个独立的业务流程管理系统,由此来协调各个系统间的集成工作,避免系统间的大量耦合。同时,我们看到定义和标准化一个大家都能理解的media
type是如此的重要。
对开放平台的支持,使得我们将业务流程管理系统开放出来,作为公有云服务平台,同时流程中的各个环节能够开放出来,作为服务/应用接入和接出的接口,真正做到松耦合,由此看到开放平台的实质是流程服务的开放。
在同事陈金洲的《架构腐化之谜》中,文章最后提到解决这一问题的方法是:创建应用程序的生态环境,而非单一的项目。深得我心,而我们这里想表达的是,随着系统的演进,我们需要不断进行系统的分解,做到服务的独立演化。
至于rest,它的作用就在于充分利用HTTP协议,将其作为一种应用协议,而不仅仅是传输协议,这样在WEB上做集成时能够最大程度上的达成一致,形成大家都能接受的约定,减少集成的工作量,对外接口一致,内部独立演化。 |