【10-2】复杂业务状态的处理(从状态者模式到FSM)
一、概述
我们平常在开发业务模块时,经常会遇到比较复杂的状态转换。比如说用户可能有新注册、实名认证中、已实名认证、禁用等状态,支付可能有等待支付、支付中、已支付等状态。OA系统里的状态处理就更多了。
遇到这些处理,很多人可能不假思索的就用最直观的 if/else 或者 switch 来判断状态的方式。但其实除了这种简单粗暴的方式,我们还有其他更好的方式来处理复杂的状态转换。
二、状态判断
我们就以支付为例,一笔订单可能有:【等待支付】、【支付中】、【已支付】等状态。对于等待支付的订单,用户可能通过第三方支付,如微信支付或支付宝进行付款,支付完成后第三方支付会回调通知支付结果。我们可能会这样来处理:
public void pay(Order order)
{
if (order.status == UNPAID)
{
order.status = PAYING;
// 处理支付
}
else
throw IllegalStateException("不能支付");
} public void paySuccess(Order order)
{
if (order.status == PAYING)
{
// 处理支付成功通知
order.status = PAID;
}
else
throw IllegalStateException("不能支付");
}
这样看起来好像没什么问题。但是假设我们允许用户多次支付完成一笔订单,于是我们需要增加一个【部分支付】状态。订单在【部分支付】状态时,可以进行下一步的支付:订单收到支付成功通知时,根据支付金额,可能会转换到【已支付】或【部分支付】状态。现在,我们不得不在pay和paySuccess里处理这个状态。
public void pay(Order order)
{
if (order.status == UNPAID || order.status == PARTIAL_PAID)
{
order.status = PAYING;
// 处理支付
}
else
throw IllegalStateException("不能支付");
} public void paySuccess(Order order)
{
if (order.status == PAYING)
{
// 处理支付成功通知
if (order.paidFee == order.totalFee)
order.status = PAID;
else
order.status = PARTIAL_PAID;
}
else
throw IllegalStateException("不能支付");
}
有了支付,我们必须也要能支持退款,那就需要增加【退款中】和【已退款】状态,以及对应的退款操作和退款成功回调处理。
public void refund(Order order)
{
if (order.status == PAID || order.status == PARTIAL_PAID)
{
order.status = REFUNDING;
// 处理退款
}
else
throw IllegalStateException("不能退款");
} public void refundSuccess(Order order)
{
if (order.status == REFUNDING)
{
// 处理退款成功通知
order.status = REFUNDED;
}
else
throw IllegalStateException("不能退款");
}
如果用一个有限状态机(FSM:Finite State Machine)来表示目前的状态转换,那大概是这样的:
对于状态不多、转换也不是很复杂的情况,用状态判断来处理也还算简洁明了。但一旦状态变多,操作变复杂,那么业务代码就会充斥各种条件判断,各种状态处理逻辑散落在各处。这时如果要新增一种状态,或者调整一些处理逻辑,就会比较麻烦,还很容易出错。
例如本例中,实际处理时可能还存在取消订单、支付失败/超时、退款失败/超时等情况,如果再加上物流以及一些内部状态,那处理起来就极其复杂了,而且一不小心还会出现支付失败了还能给用户退款,或者已经退款了还给用户发货等不应该出现的情况。这其实是一种坏味道,会造成代码不易维护和扩展。
三、设计模式之状态模式
不少人接下来可能会想到GOF的状态模式。对于涉及复杂状态逻辑的处理,使用状态模式可以将具体的状态抽象出来,而不是分散在各个方法的条件判断处理中,更容易维护和扩展。
状态模式一般包含三种角色,Context、State和ConcreteState。其中State是状态接口,定义了状态的操作。而ConcreteState则是各个具体状态的实现。它门的关系如下图所示:
下面我们尝试用状态模式实现前面的订单状态转换。首先我们需要定义状态接口,它应该包含所有需要的操作,以及每个状态对应的实现。
abstract class OrderState
{
public abstract OrderState pay(Order order);
public abstract OrderState paySuccess(Order order);
public abstract OrderState refund(Order order);
public abstract OrderState refundSuccess(Order order);
} //支付中状态
public class PayingOrderState:OrderState
{
public OrderState pay(Order order)
{
throw IllegalStateException("已经在支付中");
}
public OrderState paySuccess(Order order, long fee)
{
doPaySuccess(Order order, long fee);
if (order.paidFee < order.totalFee)
{
order.setState(new PartialPaidOrderState());
}
else
{
order.setState(new PaidOrderState());
}
}
public OrderState refund(Order order)
{
throw IllegalStateException("尚未完成支付");
}
public OrderState refundSuccess(Order order)
{
throw IllegalStateException("尚未完成支付");
}
} public class UnpaidOrder:OrderState { ... }
public class PartialPaidOrderState:OrderState { ... }
public class PaidOrderState:OrderState { ... }
public class RefundingOrderState:OrderState { ... }
public class RefundedOrderState:OrderState { ... }
大家可能会注意到,不是每个状态都支持所有操作的。例如上面的实现,PayingOrderState是不能refund的,PaidOrderState是不能Pay的,这里我们抛出了一个 IllegalStateException异常。当然也可以不抛异常,而是放一个空的实现。或者我们也可以定义一个 Abstract Class,把操作的默认实现都放到里面,每个状态类只需要改写自己支持的方法。
然后我们要实现Context,也就是我们的Order实体,它包含了一个状态字段state,通过state实现所有的状态转换逻辑。定义好了这些,支付服务的实现就很简单了。
public class Order
{
OrderState state = new UnpaidOrder();
public void pay(long fee)
{
state.pay(fee);
}
public void paySuccess(long fee)
{
state.paySuccess(this, fee);
}
public void refund() { ... }
public void refundSuccess() { ... }
} public class PaymentService
{
public void payOrder(long orderId)
{
Order order = OrderRepository.find(orderId)
order.pay();
OrderRepository.save(order);
}
}
通过状态模式,我们避免了代码里出现大量状态判断,状态转换规则的实现更加清晰。不过需要注意的是,实际上状态模式并不是很符合开闭原则(Open/Close Principle),新增一个状态时,还是可能要修改已有的其他状态的逻辑。但是和状态判断的方法比起来,已经清晰并且方便很多了。
四、领域驱动设计之状态建模
前面也提到了,状态模式的另外一个问题就是,实际业务里面有很多操作其实只对部分状态有效,而状态模式要求每个状态都要实现所有操作,有时候这是没有必要的。
对于这种情况,在领域驱动设计里,会更建议大家使用显式状态建模的方式。也就是把不同状态的实体,建模成不同的实体类;或者每个实体类代表一组状态。
例如,我们可以对每种状态的订单,都定义一个实体类。不过因为可能有多种状态的订单支持同样的操作,为了抽象这类操作,我们需要先定义一些接口。
public interface CanPayOrder
{
Order pay();
}
public interface CanPaySuccessOrder { ... }
public interface CanRefundOrder { ... }
public interface CanRefundSuccessOrder { ... } public class UnpaidOrder implements CanPayOrder { ... }
public class PayingOrder implements CanPaySuccessOrder { ... }
public class PartialPaidOrder implements CanPayOrder, CanRefundOrder { ... }
public class PaidOrder implements CanRefundOrder { ... }
public class RefundingOrder implements CanRefundSuccessOrder { ... } public class PaymentService
{
public void pay(long orderId) {
Order order = OrderRepository.find(orderId)
// 转换为 CanPayOrder,如果无法转换则抛异常
CanPayOrder orderToPay = order.asCanPayOrder();
Order payingOrder = orderToPay.pay();
OrderRepository.save(payingOrder);
}
}
每种状态的实体能支持的操作,都是显式定义好的。这种方式对于操作比较多,并且很多操作只对部分状态有效的情况,能够有效避免状态模式的缺点,代码更简洁清晰。
五、动态语言里的状态转换
上面的例子里,UnpaidOrder和PartialPaidOrder都可以进行 pay 操作。其实处理支付操作的时候,我们不需要知道它是 UnpaidOrder 还是 PartialPaidOrder,我只需要知道当前订单实体支持Pay操作就可以了。在 Java 这样的静态类型语言里,我们只能通过定义一些 Interface 或者 Abstract class 来处理,还是有一点点麻烦。
如果是动态类型语言,例如 Python、Ruby 或者 JavaScript 等,还可以通过 Duck Typing 来进一步简化。所谓 Duck Typing 就是:“如果有一只鸟,它走起来像鸭子,游起来像鸭子,叫起来也像鸭子,我们就叫它鸭子。” 意思就是说,我们可以忽略对象的类型,直接在运行时判断对象是否支持某种行为。
例如在 JavaScript 里,我们获取到 order 实体后,就可以通过判断是否定义了 pay 方法,然后直接调用即可,而不必了解对象到底是什么类型。
let orderToPay = order.asOrderStateEntity();
if (typeof orderToPay['pay'] === 'function')
{
orderToPay.pay();
}
else
{
throw new ServiceError("该订单不能进行支付操作");
}
当然,实际会用动态语言开发这种业务系统的并不多,毕竟动态语言也会引起其他方面的一些问题。
六、FSM
不管是状态模式还是状态实体,多个状态之间的转换,还是分散在各个状态的实现里的。其实所有的状态转换都可以概括为:F(S, E) -> (A, S'),即如果当前状态为S,接收到一个事件E,则执行动作A,同时状态转换为S'。
Akka里面提供了一个有限状态机的框架叫FSM,通过Scala语言的模式匹配及其他一些强大特性,可以把状态转换和业务处理逻辑分离开来。具体我就不细说了,我们也没有在实际开发中使用过。但我们可以感受一下:
class OrderFSM extends FSM[State, Data]
{
startWith(Unpaid) // 开始时的状态是 Unpaid
when(Unpaid)
{
case Event(Pay, data) ⇒ // Unpaid 状态时,如果收到事件 Pay,则进行支付,状态转换为 Paying
doPay(data)
goto(Paying)
}
when(Paying)
{ // Paying 状态时,如果收到事件 PaySuccess,则进行支付成功处理,通过根据支付金额,转换为 Paid 或者 PartialPaid 状态
case Event(PaySuccess(fee), data) ⇒
doPaySuccess(data, fee)
if (fee+data.paidFee == data.totalFee) goto(Paid)
else goto(PartialPaid)
}
// ...
}
当然,FSM 的功能远不止此,实际实现也可能会更复杂。Java 里面也有一个有限状态机的实现,叫 Squirrel,不过由于 Java 语言的限制,使用起来没有 Akka FSM 那么优雅。这里就不深入研究了,感兴趣的同学可以去了解下。
七、总结
本文简单介绍了业务系统中,处理复杂状态逻辑的几种方法。除了极其简单的情况,大家应该尽量避免使用状态判断的方式,使用状态模式或者状态建模,可以很有效的提高代码的维护性和扩展性。最后也简单介绍了动态语言对状态建模的一些优化,以及 FSM 框架。
参考链接:https://mp.weixin.qq.com/s/qe0OSiSAwVpGqYDKoWeOPQ
【10-2】复杂业务状态的处理(从状态者模式到FSM)的更多相关文章
- 使用Nagios打造专业的业务状态监控
想必各个公司都有部署zabbix之类的监控系统来监控服务器的资源使用情况.各服务的运行状态,是否这种监控就足够了呢?有没有遇到监控系统一切正常确发现项目无法正常对外提供服务的情况呢?本篇文章聊聊我们如 ...
- ARM处理器的寄存器,ARM与Thumb状态,7中运行模式
** ARM处理器的寄存器,ARM与Thumb状态,7中运行模式 分类: 嵌入式 ARM处理器工作模式一共有 7 种 : USR 模式 正常用户模式,程序正常执行模式 FIQ模式(Fast ...
- Django 小实例S1 简易学生选课管理系统 10 老师课程业务实现
Django 小实例S1 简易学生选课管理系统 第10节--老师课程业务实现 点击查看教程总目录 作者自我介绍:b站小UP主,时常直播编程+红警三,python1对1辅导老师. 课程模块中,老师将要使 ...
- 10.6 监控io性能 10.7 free命令 10.8 ps命令 10.9 查看网络状态 10.10 linux下抓包
iostat sysstat 包里面包括 sar 和 iostat [root@centos7 ~]# iostat Linux 3.10.0-693.2.2.el7.x86_64 (centos7. ...
- Linux企业级项目实践之网络爬虫(10)——处理HTTP状态码
HTTP状态码(HTTP Status Code)是用以表示网页服务器HTTP响应状态的3位数字代码.所有状态码的第一个数字代表了响应的五种状态之一.他们分别是:消息(1字头)成功(2字头)这一类型的 ...
- 学习Acegi应用到实际项目中(10)- 保护业务方法
前面已经讲过关于保护Web资源的方式,其中包括直接在XML文件中配置和自定义实现FilterInvocationDefinitionSource接口两种方式.在实际企业应用中,保护Web资源非常重要, ...
- 使用netlify-statuskit 进行系统业务状态报告
netlify-statuskit 是netlify 团队开源的一款类似github status 的脚手架website,使用此工具 我们可以对于我们系统模块进行报告,同时对于故障时,我们可以进行故 ...
- Flask基础(10)-->http的无状态协议解决办法一(客户端cookie)
http的无状态协议 http是一种无状态协议,浏览器请求服务器时无状态的 什么是无状态? 无状态:指的是一次用户请求时,浏览器.服务器无法知道之前这个用户做过什么,每次请求都是一次新的请求. 无状态 ...
- Nginx服务编译安装、日志功能、状态模块及访问认证模式实操
系统环境 [root@web ~]# cat /etc/redhat-release CentOS release 6.9 (Final) [root@web ~]# uname -a Linux d ...
随机推荐
- CentOS 7 - 配置服务实现开机自启动
新建系统服务描述文件 cd /etc/systemd/system sudo vim myapp.service 添加以下配置: [Unit] # 这里添加你的服务描述 Description=mya ...
- 设计模式《JAVA与模式》之命令模式
在阎宏博士的<JAVA与模式>一书中开头是这样描述命令(Command)模式的: 命令模式属于对象的行为模式.命令模式又称为行动(Action)模式或交易(Transaction)模式. ...
- linux下hex转ascii
场景: 在wireshark里面提取response包的值. echo -n "1f8b08000000000000030cd335b2e580014" | xxd -r -p & ...
- ThinkPHP5代码执行的简单分析
漏洞影响版本: ThinkPHP 5.0.5-5.0.22 ThinkPHP 5.1.0-5.1.30 漏洞复现: 一.mac的debug环境搭建. 一键化环境搭建工具: mamp pro ,调试工具 ...
- odoo开发笔记 -- wkhtmltox打印不显示中文 --ubuntu字体安装
wkhtmltox 是一个开源的将网页内容转换成PDF的软件包,常嵌套在网页页面里边做打印功能. 以微软雅黑字体为例(其他的宋体.黑体等点阵字体都一样的),我们的雅黑字体文件是:Yahei.ttf(放 ...
- 浏览器内核、渲染引擎、JS引擎简介
一.定义 浏览器内核分成两部分:渲染引擎和JS引擎. 由于JS引擎越来越独立,浏览器内核 就倾向于 单指 渲染引擎. 渲染引擎是一种对HTML文档进行解析并将其显示在页面上的工具.(说白了,就是按照 ...
- 关于C++11中的std::move和std::forward
std::move是一个用于提示优化的函数,过去的c++98中,由于无法将作为右值的临时变量从左值当中区别出来,所以程序运行时有大量临时变量白白的创建后又立刻销毁,其中又尤其是返回字符串std::st ...
- 一对一Socket简单聊天的实现
今天终于调试通了Socket一对一的聊天,每次发送连接请求后,将用户名发送到Socket中去,然后将用户名和新建的socket存到map中,然后根据用户名来确定接收方是谁,以实现一对一的聊天功能. 上 ...
- 选择 Python3.6 还是 Python 3.7
转自:白月黑羽在线教程:http://www.python3.vip/doc/blog/python/home/ 选择 Python3.6 还是 Python 3.7 Python 3.7 已经发布了 ...
- mysql 删除单表内多个字段重复的数据
mysql 删除单表内多个字段重复的数据 DELETE from lot_log_payflow WHERE (pay_no,sub_flow_type) in () s1) AND id ) s2) ...