ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Go设计模式:策略模式与状态模式

Go设计模式:策略模式与状态模式 Go设计模式:策略模式与状态模式摘要: 本篇讲解Go语言策略模式与状态模式的实现用策略接口封装支付方式选择逻辑用状态机接口管理订单状态流转分享状态爆炸导致类数量激增的踩坑经验对比策略模式、状态模式和简单条件分支三种方案。开篇故事上个月我们的支付服务接入了第6种支付方式。之前的代码是一坨if-else微信支付、支付宝、银联、余额、积分、礼品卡每加一种就在Pay方法里塞一个分支。第6种加进去的时候Pay方法已经膨胀到300行里面混着风控校验、渠道调用、回调处理改一处怕碰崩另外几处。我花了一个周末把支付方式拆成策略模式。每种支付方式实现同一个PaymentStrategy接口Pay方法里只做策略选择和调用。代码从300行缩到40行新增支付方式只要加一个文件。再往前推订单状态流转也有类似问题。待支付、已支付、已发货、已签收、已取消状态之间的流转规则散落在十几个方法里。改成状态模式后每个状态自己管自己能跳到哪些状态清晰了很多。这篇把两种模式的实现写清楚。一、策略模式实现策略模式的思路是把一组算法封装成独立类调用方持有策略接口运行时按需切换。Go没有类继承用接口加结构体组合实现更自然。支付场景是最典型的策略模式用例。调用方不关心用哪种支付方式只要传入金额拿到支付结果就行。packagepaymentimportfmt// PaymentResult 支付结果typePaymentResultstruct{OrderIDstring// 订单IDAmountint64// 支付金额单位分Channelstring// 支付渠道TradeNostring// 渠道流水号Successbool// 是否成功}// PaymentStrategy 支付策略接口// 所有支付方式实现这个接口typePaymentStrategyinterface{// Pay 执行支付// orderID: 订单号// amount: 金额单位分Pay(orderIDstring,amountint64)(*PaymentResult,error)// Channel 返回渠道名称用于日志和账单Channel()string}// WechatPay 微信支付策略typeWechatPaystruct{AppIDstring// 微信应用IDAppSecretstring// 应用密钥}// Pay 微信支付实现func(w*WechatPay)Pay(orderIDstring,amountint64)(*PaymentResult,error){// 实际调用微信统一下单接口// 这里简化为模拟流程tradeNo:fmt.Sprintf(WX%s,orderID)returnPaymentResult{OrderID:orderID,Amount:amount,Channel:w.Channel(),TradeNo:tradeNo,Success:true,},nil}// Channel 返回渠道名func(w*WechatPay)Channel()string{returnwechat}// Alipay 支付宝支付策略typeAlipaystruct{PartnerIDstring// 支付宝合作者IDPrivateKeystring// 商户私钥}// Pay 支付宝支付实现func(a*Alipay)Pay(orderIDstring,amountint64)(*PaymentResult,error){// 实际调用支付宝alipay.trade.prepay接口tradeNo:fmt.Sprintf(ALI%s,orderID)returnPaymentResult{OrderID:orderID,Amount:amount,Channel:a.Channel(),TradeNo:tradeNo,Success:true,},nil}// Channel 返回渠道名func(a*Alipay)Channel()string{returnalipay}// BalancePay 余额支付策略typeBalancePaystruct{UserIDstring// 用户ID}// Pay 余额扣减支付func(b*BalancePay)Pay(orderIDstring,amountint64)(*PaymentResult,error){// 实际查用户余额表扣减余额// 余额不足返回失败tradeNo:fmt.Sprintf(BAL%s,orderID)returnPaymentResult{OrderID:orderID,Amount:amount,Channel:b.Channel(),TradeNo:tradeNo,Success:true,},nil}// Channel 返回渠道名func(b*BalancePay)Channel()string{returnbalance}策略接口定义好具体策略实现完调用方怎么用。关键是搞一个策略注册表按渠道名找到对应策略。packagepaymentimporterrors// PaymentService 支付服务持有所有策略typePaymentServicestruct{strategiesmap[string]PaymentStrategy// 渠道名到策略的映射}// NewPaymentService 创建支付服务funcNewPaymentService()*PaymentService{returnPaymentService{strategies:make(map[string]PaymentStrategy),}}// RegisterStrategy 注册支付策略// channel: 渠道名如wechatfunc(ps*PaymentService)RegisterStrategy(channelstring,s PaymentStrategy){ps.strategies[channel]s}// Pay 支付入口按渠道选择策略// channel: 用户选择的支付渠道// orderID: 订单号// amount: 金额单位分func(ps*PaymentService)Pay(channel,orderIDstring,amountint64)(*PaymentResult,error){// 查找对应渠道的策略strategy,ok:ps.strategies[channel]if!ok{returnnil,errors.New(不支持的支付渠道: channel)}// 调用策略执行支付returnstrategy.Pay(orderID,amount)}新增支付方式只要写一个结构体实现PaymentStrategy接口注册到PaymentService调用方代码一行不用改。这是策略模式的核心价值对扩展开放对修改封闭。二、状态模式实现状态模式的思路是把对象的每个状态封装成独立类型状态流转逻辑分散到各个状态类型里。对象持有当前状态行为调用委托给状态对象处理。订单流转是典型场景。待支付状态只能跳到已支付或已取消已支付状态只能跳到已发货已发货状态只能跳到已签收。每个状态知道自己能跳到哪违规跳转直接拒绝。packageorderimport(errorsfmt)// OrderStatus 订单状态类型typeOrderStatusstringconst(StatusPending OrderStatuspending// 待支付StatusPaid OrderStatuspaid// 已支付StatusShipped OrderStatusshipped// 已发货StatusReceived OrderStatusreceived// 已签收StatusCanceled OrderStatuscanceled// 已取消)// Order 订单实体typeOrderstruct{IDstring// 订单号Status OrderStatus// 当前状态Amountint64// 订单金额}// State 状态接口每个状态实现这个接口// 订单的流转操作委托给当前状态处理typeStateinterface{// Pay 处理支付操作Pay(o*Order)error// Ship 处理发货操作Ship(o*Order)error// Receive 处理签收操作Receive(o*Order)error// Cancel 处理取消操作Cancel(o*Order)error// Name 返回状态名Name()OrderStatus}// PendingState 待支付状态typePendingStatestruct{}func(s*PendingState)Pay(o*Order)error{// 待支付可以跳到已支付o.StatusStatusPaidreturnnil}func(s*PendingState)Ship(o*Order)error{// 待支付不能直接发货returnerrors.New(待支付状态无法发货)}func(s*PendingState)Receive(o*Order)error{returnerrors.New(待支付状态无法签收)}func(s*PendingState)Cancel(o*Order)error{// 待支付可以取消o.StatusStatusCanceledreturnnil}func(s*PendingState)Name()OrderStatus{returnStatusPending}// PaidState 已支付状态typePaidStatestruct{}func(s*PaidState)Pay(o*Order)error{returnerrors.New(已支付无需重复支付)}func(s*PaidState)Ship(o*Order)error{// 已支付可以发货o.StatusStatusShippedreturnnil}func(s*PaidState)Receive(o*Order)error{returnerrors.New(已支付但未发货无法签收)}func(s*PaidState)Cancel(o*Order)error{// 已支付需要走退款流程这里简化returnerrors.New(已支付订单取消需走退款)}func(s*PaidState)Name()OrderStatus{returnStatusPaid}// ShippedState 已发货状态typeShippedStatestruct{}func(s*ShippedState)Pay(o*Order)error{returnerrors.New(已发货无需支付)}func(s*ShippedState)Ship(o*Order)error{returnerrors.New(已发货无需重复发货)}func(s*ShippedState)Receive(o*Order)error{// 已发货可以签收o.StatusStatusReceivedreturnnil}func(s*ShippedState)Cancel(o*Order)error{returnerrors.New(已发货订单无法取消请走退货流程)}func(s*ShippedState)Name()OrderStatus{returnStatusShipped}// stateRegistry 状态注册表状态名到状态对象varstateRegistrymap[OrderStatus]State{StatusPending:PendingState{},StatusPaid:PaidState{},StatusShipped:ShippedState{},}// GetState 根据状态名获取状态对象funcGetState(status OrderStatus)State{returnstateRegistry[status]}// Pay 订单支付委托给当前状态func(o*Order)Pay()error{state:GetState(o.Status)ifstatenil{returnfmt.Errorf(未知状态: %s,o.Status)}returnstate.Pay(o)}// Ship 订单发货委托给当前状态func(o*Order)Ship()error{state:GetState(o.Status)ifstatenil{returnfmt.Errorf(未知状态: %s,o.Status)}returnstate.Ship(o)}// Receive 订单签收委托给当前状态func(o*Order)Receive()error{state:GetState(o.Status)ifstatenil{returnfmt.Errorf(未知状态: %s,o.Status)}returnstate.Receive(o)}状态模式把流转规则分散到各个状态类型里新增状态只要加一个State实现并注册不影响其他状态。订单对象的方法只做委托逻辑非常薄。三、踩坑经验:状态爆炸导致类数量激增这个坑出现在我们的工单系统。工单状态比订单复杂得多有创建、待分配、处理中、待确认、已解决、已关闭、已重开、已挂起8个状态。第一版用状态模式实现每个状态一个类型8个状态8个文件每个文件里4到6个方法。问题出在状态之间的流转组合。8个状态两两组合就是64种流转判断每个状态类型里都要写7个方法判断能不能跳到其他7个状态。代码量爆炸而且很多流转是非法的比如已关闭不能跳到处理中这些非法流转的逻辑散落在各处维护起来要翻8个文件。更麻烦的是重开状态。工单关闭后用户不满意可以重开重开后回到处理中。但处理中状态不知道工单之前经过哪些状态重开后的流转规则和新工单不完全一样状态模式处理不了这种带历史的状态。解决方案是引入有限状态机(FSM)库用状态转移表代替状态类。状态转移表集中定义所有合法流转非法流转默认拒绝。packageworkflowimport(errorsfmtsync)// Transition 状态转移定义typeTransitionstruct{Fromstring// 起始状态Eventstring// 触发事件Tostring// 目标状态Guardfunc(ctxmap[string]interface{})bool// 守卫条件可选}// FSM 有限状态机typeFSMstruct{mu sync.Mutex currentstring// 当前状态transitions[]Transition// 转移规则表handlersmap[string]func(string,string)// 状态变更回调}// NewFSM 创建状态机// initial: 初始状态funcNewFSM(initialstring)*FSM{returnFSM{current:initial,transitions:make([]Transition,0),handlers:make(map[string]func(string,string)),}}// AddTransition 添加转移规则func(f*FSM)AddTransition(from,event,tostring,guardfunc(map[string]interface{})bool){f.transitionsappend(f.transitions,Transition{From:from,Event:event,To:to,Guard:guard,})}// Send 触发事件尝试状态转移func(f*FSM)Send(eventstring,ctxmap[string]interface{})error{f.mu.Lock()deferf.mu.Unlock()// 查找匹配的转移规则for_,t:rangef.transitions{ift.Fromf.currentt.Eventevent{// 检查守卫条件ift.Guard!nil!t.Guard(ctx){returnfmt.Errorf(守卫条件不满足: %s,event)}old:f.current f.currentt.To// 触发回调ifh,ok:f.handlers[event];ok{h(old,t.To)}returnnil}}// 没有匹配规则拒绝转移returnfmt.Errorf(非法转移: %s - %s,f.current,event)}// Current 返回当前状态func(f*FSM)Current()string{f.mu.Lock()deferf.mu.Unlock()returnf.current}// OnTransition 注册状态变更回调func(f*FSM)OnTransition(eventstring,handlerfunc(from,tostring)){f.handlers[event]handler}用转移表替代状态类8个状态16条合法流转规则全部集中在一个表里新增状态只要加几行规则。守卫条件支持基于上下文的判断重开时传入工单历史守卫函数检查是否符合重开条件。四、对比分析方案扩展性代码量流转规则集中度适合场景条件分支差少集中但臃肿状态少于3个策略模式好中不涉及流转算法选择场景状态模式中多分散在各状态类状态少且流转简单状态机表好中集中在转移表状态多且流转复杂策略模式适合算法可替换的场景状态模式适合状态流转规则简单的场景。状态多了之后状态模式会膨胀转移表式的状态机更合适。条件分支简单场景还能用状态超过5个就别硬撑了维护成本远高于重构。总结策略模式把算法封装成接口实现调用方按需选择新增策略不影响现有代码。状态模式把每个状态封装成类型流转规则分散到状态类型里。状态数量少时状态模式清晰状态多了会爆炸改用状态机转移表集中管理。两种模式的本质都是用接口隔离变化把容易变化的部分独立出来。
返回列表