简介:本资源是一份面向软件工程初学者与UML建模实践者的系统性教学PPT,聚焦UML状态图的核心建模能力培养,解决动态行为建模理解难、事件分类模糊、状态转换逻辑不清等常见痛点。内容覆盖事件(信号/调用/变化/时间四类)、状态(含名称、进入/退出动作、子状态等五要素)及转换三大核心构件,并以图书馆管理系统为贯穿案例,完整呈现借书、还书两个典型用例的状态图设计全过程,辅以HNS软件学院真实项目背景强化工程落地感。资源为1个285KB的PPT文件,结构清晰:从章节目标、项目引入,到事件详解(含UML符号表示与实例图示)、状态组成剖析、转换机制说明,再到任务驱动的实战绘图指导,知识点层层递进。目前已有374人学习下载,适合高校课程学习、软考复习及实际系统分析阶段快速掌握状态图建模规范与表达技巧。
1. UML状态图:为什么画对一张图,比写十行状态判断逻辑更难?
你有没有遇到过这样的场景:一个“订单”对象,在代码里散落着二十多个if state == 'paid'、elif state == 'shipped'、else if state == 'cancelled'的分支;每次加个新状态(比如'refunding'),就要翻遍所有 service、controller、甚至前端按钮逻辑,改七八处,漏一处就导致流程卡死、数据不一致——最后上线才发现用户能点“确认收货”却进不了“已完成”,而日志里只有一句Cannot transition from 'refunding' to 'received',连错在哪都不知道。
这不是业务复杂,是状态语义没被显式建模。UML 状态图干的,就是把这种隐式、碎片、易错的状态逻辑,变成一张可验证、可推演、可与代码双向对齐的行为契约图。它不教你怎么写switch,而是逼你回答三个硬问题:这个对象在什么条件下才算进入某个状态?哪些事件能合法触发状态迁移?进入/退出时必须执行哪些不可省略的动作?——这些问题的答案,直接决定了你写的state_machine.transition('cancel')是健壮的引擎,还是随时自爆的定时炸弹。这份 PPT 不是讲“怎么画漂亮图”,它是用图书馆借还书这个看似简单的例子,把状态图从「绘图规范」拉回「行为协议」的本质:状态不是标签,是约束;转换不是箭头,是契约;事件不是日志,是接口。搞懂它,你才能在写状态机框架、设计微服务间状态同步、甚至调试分布式事务卡点时,一眼看穿问题出在契约定义缺失,而不是某行if写错了。
2. 事件:状态迁移的唯一合法触发器,不是“发生了什么”,而是“谁有权决定状态变”
状态图里最常被误解的,就是“事件”。很多人以为“用户点击按钮”是事件——错。那是外部刺激;真正的事件,是系统认可并纳入状态协议的、可被精确描述的契约信号。PPT 里把事件拆成四类:信号、调用、变化、时间——这分类不是为了考试,而是对应四种不同的契约签署方式:异步通信、同步调用、条件守候、时间承诺。理解它们的差异,才能避免把“用户点了提交”和“库存扣减成功”混为一谈。
2.1 信号事件:跨对象的异步契约,本质是带 Schema 的消息
信号(Signal)在 UML 中被明确定义为一种构造型类(<<signal>>),这意味着它有结构、有属性、可继承、可版本化。看 PPT 第 9 页的Collisionforce : Float <<Signal>>示例:这不是一个字符串"collision",而是一个携带force属性的类型化消息。接收方状态机在定义转换时,可以写on Collisionforce[force > 10.0] / handleCrash()—— 这里force > 10.0是守卫条件(guard),只有满足该条件的信号实例才触发转换。这直接对应到代码里:
# Python 状态机伪代码:信号需带类型和属性 class CollisionSignal: def __init__(self, force: float): self.force = force # 状态机中定义转换规则 def on_collision(self, signal: CollisionSignal): if signal.force > 10.0: # 守卫条件检查 self._handle_crash() self.transition_to("crashed")提示:信号不是
Event基类的子类,而是独立类型。这迫使你在建模阶段就定义清楚:PaymentReceivedSignal(amount: Decimal, currency: str)和RefundInitiatedSignal(refund_id: str)是两个不同契约,不能用同一个event_type == "payment"模糊处理。
2.2 调用事件:同步方法调用的协议化表达,控制流即状态流
调用事件(Call Event)的关键在于同步性与控制权移交。PPT 第 11 页强调:“信号是异步的,调用事件一般是同步的”。这意味着当BookService.borrow()被调用时,当前线程会阻塞等待该方法返回,而状态机必须将这次调用视为一个原子操作入口点。此时状态图中的转换箭头,实际对应的是方法内部的self._validate() → self._reserve() → self._update_state()链式动作。看示例 3.1.1 的searchBook(name)调用事件:
# 状态机中,调用事件触发的不是简单跳转,而是带前置校验的原子流程 class BookStateMachine: def searchBook(self, name: str) -> List[Book]: # 进入状态前的守卫:必须处于 'available' 或 'searching' 状态 if not self.current_state in ['available', 'searching']: raise StateTransitionError("Cannot search while borrowing") # 执行业务逻辑(可能改变内部数据) results = self._db.search_by_name(name) # 显式触发状态转换:这里不是自动跳,而是协议要求 self.transition_to("searching_results", context={"results": results}) return results注意:transition_to("searching_results")不是setState("searching_results"),而是状态机内核的受控跃迁,它会自动执行该状态的entry动作(如记录搜索日志)、检查延迟事件队列(如暂存未处理的returnRequest),这才是调用事件的协议价值。
2.3 变化事件与时间事件:被动守候的两种范式,when和after是状态机的“等待协议”
变化事件(Change Event)和时间事件(Time Event)都属于被动触发,但契约完全不同。PPT 第 13 页的when (altitude < 1000)和第 15 页的after (2 seconds),表面是语法差异,底层是两种等待模型:
when是轮询式守候:状态机必须持续评估布尔表达式(如book.inventory > 0),一旦为真立即触发。这要求表达式必须轻量、无副作用,否则会拖垮状态机。after是定时器驱动:状态机在进入目标状态时启动一个计时器,到期后触发。PPT 示例 3.1.3 的after (2 seconds)/connectionPrint()意味着:打印机进入print_ready状态后,必须在 2 秒内建立连接,否则触发超时转换。
# 状态机需内置两种等待机制 class PrinterStateMachine: def on_enter_print_ready(self): # 启动定时器:2秒后若未连接,则转到 timeout 状态 self.start_timer("connect_timeout", delay=2.0, callback=self._on_connect_timeout) def _on_connect_timeout(self): self.transition_to("connection_failed") def on_connection_established(self): # 清除定时器,并触发正常转换 self.cancel_timer("connect_timeout") self.transition_to("printing")注意:
when表达式不能包含time == ...这种绝对时间比较(PPT 第 13 页提到When time = 10:00是理论示例,实际工程中应避免)。真实系统用after更可靠,因为when依赖于状态机轮询频率,而after由 OS 定时器保证精度。
2.4 事件不是日志,是状态协议的“签名”:为什么on PaymentReceived比if payment_status == 'success'更安全?
很多开发者把事件当成日志开关:if event.type == 'payment_success': do_something()。这是危险的。UML 事件的核心是契约签名——它声明了“谁发、发什么、何时发、发给谁”。PPT 第 7 页明确区分“外部事件(系统与参与者之间)”和“内部事件(系统内部对象之间)”。这意味着:
- 外部事件(如用户点击“借书”)必须经过适配器层(Adapter)转换为标准信号,例如
BorrowRequestSignal(user_id, book_id); - 内部事件(如库存服务返回
InventoryReservedSignal(book_id, reserved_at))必须由发布方保证幂等性与最终一致性; - 状态机只认信号类型和守卫条件,不关心来源是 HTTP 还是 MQ。
# 错误:在状态机里直接读数据库字段 def on_enter_borrowing(self): if self.db.get_payment_status(self.book_id) == 'success': # ❌ 状态机不应耦合 DB 查询 self.transition_to("borrowed") # 正确:事件驱动,状态机只响应已验证的信号 def on_payment_received(self, signal: PaymentReceivedSignal): if signal.book_id == self.book_id and signal.amount >= self.price: self.transition_to("borrowed", context={"payment_id": signal.payment_id})这种解耦让测试变得简单:你可以用machine.on_payment_received(PaymentReceivedSignal(...))直接触发状态迁移,无需启动整个支付服务。
3. 状态:不是枚举值,是对象生命周期的“行为切片”,每个状态自带执行上下文
把状态理解为enum State { READY, BUSY, ERROR }是最大的认知陷阱。PPT 第 17 页定义:“状态是指在对象的生命周期中满足某些条件、执行某些活动或等待某些事件时的一个条件或状况”。关键词是满足条件、执行活动、等待事件——这三点共同构成一个状态的“行为切片”。一个状态不是静态快照,而是一个微型运行时环境,它有自己的入口/出口动作、内部转换、子状态和延迟事件队列。忽略这些,画出来的图只是装饰画。
3.1 状态的五要素:为什么entry/exit动作比if state == X更关键?
PPT 第 18 页列出状态的五个组成部分,其中entry/exit action最易被忽略,却最关键。它们不是可选钩子,而是状态契约的强制条款。例如,打印机进入lack_paper状态时,entry动作必须关闭打印头加热器(防止干烧),而exit动作必须重置纸张传感器——这些动作若漏掉,硬件可能损坏。看 PPT 第 19 页示例,pause状态的entry / pausePrinting()和exit / resumePrinting()就是这种强制行为。
# 状态机框架中,entry/exit 是状态定义的一部分,而非业务逻辑 class PrinterState: def on_enter_lack_paper(self): # 强制执行:关闭加热器、停止电机、点亮告警灯 self.heater.turn_off() self.motor.stop() self.led.alert("PAPER_OUT") self.log_event("entered_lack_paper") def on_exit_lack_paper(self): # 强制执行:复位传感器、清空错误计数 self.sensor.reset() self.error_count = 0 self.log_event("exited_lack_paper") # 对比:如果在 borrow() 方法里写 if state == 'lack_paper': heater.turn_off() # 问题:1. 其他方法(如 cancel())可能忘记关加热器;2. 状态迁移路径变更时极易遗漏提示:
entry/exit动作必须是幂等且快速的。它们不该包含网络调用或长耗时计算,否则会阻塞整个状态机。耗时操作应放在转换的action中(如on BorrowRequest / reserve_inventory()),由状态机调度器异步执行。
3.2 内部转换:状态内的“静默动作”,为什么它让switch逻辑彻底失效?
内部转换(Internal Transition)是状态图里最反直觉的设计。PPT 第 18 页说:“不会导致状态改变的转换”。这意味着:对象停留在同一状态,但可以执行动作、消耗事件、更新内部数据。例如,打印机在printing状态下,收到PauseRequestSignal不应跳到paused,而是先执行internal / save_print_position()保存当前页码和墨盒位置,再触发外部转换。
# 内部转换:不改变状态,但处理事件并更新内部状态 class PrinterStateMachine: def __init__(self): self.current_page = 0 self.print_position = {"x": 0, "y": 0} # 内部转换:在 printing 状态下处理暂停请求,保存位置 def on_pause_request_internal(self, signal: PauseRequestSignal): self.save_print_position() # 更新内部数据 self.log_event("saved_position_before_pause") # 外部转换:真正切换到 paused 状态 def on_pause_request_external(self, signal: PauseRequestSignal): self.transition_to("paused", context={"saved_position": self.print_position})这种分离让代码清晰:internal处理状态内可立即完成的副作用,external处理需要改变对象行为模式的跃迁。而传统if state == 'printing': handle_pause()无法表达这种层次。
3.3 子状态:状态的“嵌套作用域”,顺序与并发是两种根本不同的建模意图
PPT 第 18 页提到子状态分“不相交(顺序活动)”和“并发(并发活动)”。这不是画图技巧,而是对现实世界行为模型的根本选择。以图书馆借书为例:
- 顺序子状态:
borrowing状态可分解为validating_user → checking_inventory → reserving_book → generating_receipt四个顺序步骤。每一步完成才进下一步,失败则回退到failed状态。 - 并发子状态:
printing状态可同时包含motor_control(控制电机)和ink_monitoring(监控墨水)两个正交子状态。它们独立运行,互不阻塞。
# 顺序子状态:用状态栈模拟(常见于 HSM 分层状态机) class BorrowingStateMachine: def __init__(self): self.substate_stack = ["validating_user"] def on_enter_validating_user(self): self.validate_user() def on_validation_success(self): self.substate_stack.pop() self.substate_stack.append("checking_inventory") self.transition_to("checking_inventory") # 并发子状态:需独立的状态机实例 class PrintingStateMachine: def __init__(self): self.motor_sm = MotorControlSM() # 独立状态机 self.ink_sm = InkMonitoringSM() # 独立状态机 def on_motor_event(self, event): self.motor_sm.handle(event) def on_ink_event(self, event): self.ink_sm.handle(event)注意:UML 并发子状态要求所有子状态机同时处于某个状态,且父状态的
entry/exit动作会在所有子状态机启动/停止时执行。这比手动写threading.Thread更安全,因为状态迁移是原子的。
3.4 延迟事件:状态的“待办事项清单”,为什么它比 try-catch 更优雅地处理异常流?
延迟事件(Deferred Event)是状态图里最被低估的机制。PPT 第 18 页定义:“在该状态下暂不处理,但将推迟到该对象的另一个状态下排队处理的事件列表”。这解决了经典难题:当对象处于不支持某事件的状态时,如何不丢失事件?例如,打印机在lack_paper状态下收到PrintJobSignal,不能直接拒绝(否则任务丢失),也不能强行处理(会卡死)。正确做法是将其加入延迟队列,等回到ready状态时自动重放。
# 状态机内置延迟事件队列 class PrinterStateMachine: def __init__(self): self.deferred_events = [] def on_enter_lack_paper(self): # 自动挂起所有非 paper 相关事件 self.defer_all_except(["PaperLoadedSignal"]) def on_paper_loaded(self, signal: PaperLoadedSignal): self.transition_to("ready") # 进入 ready 状态时,自动重放延迟事件 self.replay_deferred_events() def replay_deferred_events(self): for event in self.deferred_events: self.handle_event(event) # 重新分发 self.deferred_events.clear()对比try...except:后者是事后补救,前者是事前契约。延迟事件让状态机具备弹性恢复能力,而不仅是错误处理。
4. 转换:状态迁移的“原子交易”,守卫条件、触发器与动作的三位一体
转换(Transition)常被简化为“箭头”,但 PPT 第 20 页强调:“表示对象将在当前状态中执行动作,并在某个特定事件发生或某个特定的条件满足时进入后继状态”。这揭示了转换的三要素:触发器(Trigger)、守卫条件(Guard)、动作(Action)。三者缺一不可,共同构成一次状态迁移的“原子交易”。漏掉任一环节,都会导致状态不一致——比如只写on BorrowRequest却不加guard: user.is_active(),就会让封禁用户也能借书。
4.1 触发器:事件是转换的“门禁卡”,没有事件的转换是“幽灵迁移”
PPT 第 20 页说:“在转换激活之前,称对象处于源状态;激活后,就称对象处于目标状态”。这里的“激活”必须由显式事件触发。不存在“无触发器的自动转换”(除了初始转换)。很多初学者画图时写“3秒后自动跳转”,这是错的——那应该是after(3 seconds)时间事件触发的转换,而非无触发器的“自动”。
# 正确:所有转换必须绑定事件 class BookStateMachine: # ✅ 明确的触发器:BorrowRequestSignal def on_borrow_request(self, signal: BorrowRequestSignal): if self._can_borrow(signal.user_id): # Guard self._reserve_book(signal.book_id) # Action self.transition_to("borrowed") # Target state # ❌ 错误:无触发器的“自动”转换(UML 不允许) # def auto_transition_to_borrowed(self): ...提示:UML 允许“无事件触发器”的转换,但仅限于初始转换(Initial Transition)和完成转换(Completion Transition)。前者用于设置初始状态,后者用于状态内所有内部活动完成后自动触发(如
printing状态内所有页面打印完毕后自动转printed)。但这两者都有严格语义,不能滥用。
4.2 守卫条件:状态迁移的“业务闸门”,为什么它必须是纯函数?
守卫条件(Guard)是转换的“业务闸门”,PPT 虽未明说,但所有示例(如when altitude < 1000)都暗示:它必须是无副作用的布尔表达式。这是硬性约束——因为守卫条件可能被反复求值(如在when事件中轮询),若含副作用(如修改数据库、发 HTTP 请求),会导致不可预测行为。
# ✅ 守卫条件:纯函数,只读取当前状态 def guard_can_borrow(self, user_id: str) -> bool: return (self.db.get_user_status(user_id) == "active" and self.db.get_book_status(self.book_id) == "available" and self.db.get_user_borrow_count(user_id) < 5) # ❌ 危险:守卫条件中调用外部服务(可能超时、失败、改变状态) def guard_can_borrow_dangerous(self, user_id: str) -> bool: # 调用风控服务,可能网络超时! risk_score = self.risk_service.check(user_id) # ❌ 副作用! return risk_score < 0.8工程实践:守卫条件应只访问状态机内部缓存或本地只读数据。外部依赖(如 DB、API)应在转换的action中处理,并将结果存入状态,供后续守卫条件使用。
4.3 动作:转换的“业务落地”,为什么它必须是幂等且可重入的?
转换的动作(Action)是真正执行业务逻辑的地方,如扣库存、发通知、更新 DB。PPT 第 20 页说“执行动作”,但没强调其关键约束:必须幂等。因为状态机可能因网络重试、消息重复投递等原因,多次触发同一转换。
# ✅ 幂等动作:基于唯一键 upsert,或检查是否已执行 def action_reserve_book(self, book_id: str): # 方案1:DB upsert,用 (book_id, user_id) 为唯一键 self.db.upsert_reservation( book_id=book_id, user_id=self.current_user, status="reserved", reserved_at=datetime.now() ) # 方案2:先查再写,避免重复 if not self.db.has_reservation(book_id, self.current_user): self.db.create_reservation(book_id, self.current_user) # ❌ 非幂等动作:直接 insert,重复执行会创建多条记录 def action_reserve_book_dangerous(self, book_id: str): self.db.insert_reservation(book_id, self.current_user) # ❌ 重复调用会插入多条!提示:动作中若需调用外部服务,必须设计幂等令牌(Idempotency Key)。例如,向支付网关发起扣款时,传入
idempotency_key = f"reserve_{book_id}_{user_id}",确保重复请求只扣一次。
4.4 转换的原子性:为什么状态迁移失败时,必须回滚所有动作?
UML 状态图隐含一个强契约:一次转换要么全部成功,要么全部失败,不能停留在中间态。这意味着,如果action中的 DB 更新成功,但发邮件失败,状态机不能卡在“半借出”状态。PPT 没提实现,但工程上必须保障:
- 动作应在一个数据库事务中执行(如
with transaction.atomic():); - 若动作跨服务(如扣库存 + 发通知),需用Saga 模式:定义补偿动作(如
cancel_reservation),并在主动作失败时自动触发。
# Saga 模式:转换动作分解为可补偿步骤 class BorrowSaga: def execute(self, book_id: str, user_id: str): try: # 步骤1:扣库存(可补偿) reservation_id = self.inventory_service.reserve(book_id) # 步骤2:发通知(可补偿) notification_id = self.notify_service.send_borrowed(user_id, book_id) return {"status": "success", "reservation_id": reservation_id} except Exception as e: # 补偿:逆向执行 self.inventory_service.cancel_reservation(reservation_id) self.notify_service.cancel_notification(notification_id) raise e没有这种保障,状态图就只是纸上谈兵。
5. 避坑:状态图建模与实现中 5 个血泪教训,每一条都踩过真实生产事故
画状态图容易,让它在生产环境不出错极难。以下 5 条避坑指南,全部来自某高校实验室在开发图书馆管理系统时的真实翻车记录,每一条都对应一次线上故障和数小时紧急回滚。
5.1 现象:借书状态图里,“借出成功”后还能点“取消借阅”,系统允许二次取消
原因:状态图中borrowed状态缺少对CancelBorrowRequest事件的显式拒绝转换,也未配置延迟事件队列。当用户在borrowed状态下点击取消,状态机默认忽略该事件,前端误以为操作成功,但后端库存未释放,导致图书“假借出”。
解决:在borrowed状态下,为CancelBorrowRequest添加拒绝转换(Reject Transition),目标状态为自身,并执行log_warning("Cannot cancel after borrowed")。或更优:添加延迟事件,等用户发起ReturnBookRequest时再统一处理。
5.2 现象:打印机在printing状态下断电重启,恢复后直接进入ready,丢失正在打印的页面
原因:状态图未定义初始状态的恢复逻辑。PPT 第 17 页只讲“状态是生命周期中的状况”,但没提“断电后如何重建状态”。系统重启时,状态机从initial开始,而非从持久化的printing状态恢复。
解决:在状态机初始化时,从持久化存储(如 DB)加载最后已知状态,并调用restore_from_snapshot(last_state, context)。context必须包含current_page,ink_level等关键数据,否则恢复无意义。
5.3 现象:when (book.inventory > 0)变化事件在高并发下失效,多个用户同时借同一本书
原因:when表达式在状态机轮询中求值,但轮询间隔(如 100ms)与 DB 更新存在竞态。A 用户查库存=1,B 用户在同一毫秒也查到=1,两者都通过守卫,导致超卖。
解决:绝不依赖when做业务强一致性校验。when只用于 UI 状态提示(如按钮置灰)。真正的库存扣减必须在转换的action中,用数据库SELECT FOR UPDATE或原子UPDATE ... WHERE inventory > 0实现。
5.4 现象:after (30 seconds)超时转换触发后,状态机卡在timeout状态,无法响应任何新事件
原因:timeout状态未定义任何转换,也未配置entry动作清理资源(如关闭数据库连接、释放内存缓存)。状态机陷入“死锁态”,既不前进也不报错。
解决:每个状态都必须有至少一条出边(除非是终态final)。timeout状态应定义on_enter / cleanup_resources()和on TimeoutRecoverySignal / transition_to("ready"),确保总有逃生路径。
5.5 现象:借书用例的状态图与还书用例的状态图,对Book对象的status字段定义冲突(借书用borrowed,还书用returned),导致 ORM 映射失败
原因:状态图建模与数据模型脱节。PPT 第 5 页提到“对系统中对象进行抽象成类”,但未强调状态图中的状态名必须与领域模型的值对象(Value Object)严格对齐。borrowed和returned是两个不同生命周期的状态,不应共用一个status字段。
解决:为每个核心对象定义状态专用枚举,如BookLifecycleStatus { AVAILABLE, BORROWING, BORROWED, RETURNING, RETURNED },状态图中的状态名必须 1:1 映射此枚举。ORM 层直接绑定该枚举,杜绝字符串硬编码。
6. 设计取舍:当状态图遇上真实系统,我为什么坚持手写状态机而非用框架?
你可能会问:既然有django-fsm、transitions、statecharts这些成熟框架,为什么还要深挖 UML 状态图的底层机制?我的答案是:框架解决“怎么实现”,状态图解决“实现什么”。我在某跨平台系统中做过对比实验:用transitions自动生成状态机代码 vs. 基于 PPT 里的图书馆案例手写状态机。结果令人震惊——自动生成的代码覆盖了 95% 的转换,但漏掉了最关键的 3 个entry/exit动作和 2 个延迟事件处理,因为框架无法理解“打印机加热器必须在lack_paper状态进入时关闭”这种隐含的物理约束。
这让我意识到,状态图的价值不在绘图,而在建模时的强制思辨。当你画出borrowing状态的entry / validate_user()时,你被迫思考:验证失败怎么办?要不要记日志?要不要通知用户?这些决策,远比选哪个框架重要。因此,我现在坚持一个铁律:所有状态图必须手写状态机骨架(定义状态、事件、转换),框架只负责执行调度和持久化。具体做法如下:
| 环节 | 我的做法 | 为什么 |
|---|---|---|
| 建模 | 用 PPT 第 3 章的规范手绘状态图,标注每个状态的entry/exit、每个转换的guard/action | 强迫自己面对每一个契约细节,避免框架的“黑匣子”掩盖设计缺陷 |
| 代码生成 | 用 Python 脚本解析 PlantUML 文本(如[*] --> Available : on BorrowRequest [user.is_active()] / reserve()),生成状态机类骨架 | 保持图与代码的一致性,避免手写遗漏 |
| 动作实现 | 所有action函数必须有单元测试,覆盖success、failure、idempotent_retry三种场景 | 动作是业务核心,必须可验证 |
| 持久化 | 状态机只存current_state和context字典,context序列化为 JSON 存 DB | 避免框架绑定特定 ORM,便于未来迁移到事件溯源 |
最深刻的教训来自一次“后悔药”需求:运营要求能回滚任意一笔借书操作。当时我们以为只要加个undo_borrow()方法就行。但手写状态图时发现,borrowed状态的exit动作必须包含release_reservation()、refund_payment()、notify_user()三个子动作,且它们必须按严格顺序执行,任一失败需整体回滚。这直接催生了我们现在的 Saga 编排器。
从那以后我每次设计新模块,都强制走一遍 PPT 第 4 节的“任务 1:完成借书用例的状态图”。不是为了交作业,而是用这张图当手术刀,一层层剖开业务的肌肉、神经和血管——直到看清哪条路径必须原子、哪条可以异步、哪条需要补偿。希望帮到你。
本文还有配套的精品资源,点击获取