【设计模式精讲】3.统一建模语言(UML)速览
【设计模式精讲】3.统一建模语言UML速览【摘要】同一段设计有人用两百条群消息仍说不清有人用一张图五分钟达成共识。UML 的价值不是把系统画得复杂而是选择合适视角准确回答“有哪些角色”“谁依赖谁”“运行时谁先调用谁”“对象经历哪些状态”。本文以 Mermaid 为绘图工具介绍类图、序列图、状态机图、用例图和活动图重点讲透前三种。类图部分不再把不同关系硬排成一条强弱链而是区分使用与整体关系、类型关系并给出与 C 所有权和多态机制一致的代码随后用订单事件和订单生命周期演示时序与状态表达最后总结选图方法和常见误区。【关键词】UML、类图、序列图、状态机图、Mermaid、C1. 白板五分钟群里两小时需求是“订单支付成功后通知库存和积分系统”。技术群里很快出现了几种意见订单直接调库存最简单直接调用耦合太死可以发布事件事件又由谁维护两小时过去每个人脑中都有一套结构讨论的却未必是同一个方案。老周拉了个短会在白板上画出订单、事件总线和两个订阅者再补几根带方向的箭头。他先用类图说明“谁依赖谁”再用序列图说明“支付完成后谁先收到通知”。五分钟后团队终于围绕同一份设计讨论异常、顺序和生命周期而不是继续猜对方的意思。UMLUnified Modeling Language统一建模语言就是一套标准化的建模词汇。它不能替代需求、代码和测试也不会自动产生好设计它的作用是选择一个视角压缩沟通成本并把隐含假设暴露出来。本专栏统一使用 Mermaid。Mermaid 是适合写进 Markdown 的文本绘图语法便于版本管理和评审它能表达常用 UML 图但不是 UML 规范本身也没有覆盖全部符号。我们的目标不是画出“最标准、最完整”的图而是画出团队能正确理解、能和代码互相核对的图。2. 五种常用图各自回答什么问题UML 定义了多种图工程实践中常见的可以先掌握下面五种图观察视角主要回答的问题本专栏用途类图静态结构有哪些类型它们是什么关系各模式的结构骨架序列图运行时交互谁先调用谁消息怎样返回行为型模式的协作过程状态机图生命周期对象有哪些状态怎样流转状态模式与业务状态用例图系统边界谁使用系统获得什么能力需求阶段辅助说明活动图业务流程步骤、分支和并行怎样组织描述流程性逻辑选图前先问“我想回答什么”不要先问“这一页应该放哪种 UML”。同一个订单系统可以同时拥有五种图它们不是重复而是从不同角度观察同一对象。后续模式篇的主力是类图遇到复杂协作时补序列图遇到状态驱动行为时补状态机图。3. 类图先读类再读关系类图描述的是静态结构。阅读一张类图时先看每个框代表什么再看框之间的线。3.1 一个类框里有什么典型类框分三层名称、属性和操作。成员前的可见性符号通常是公有、-私有、#保护、~包可见。Mermaid 中还可以用interface、abstract等标记说明角色性质。Order-id: string-total: doublepay() : : voidcancel(reason) : : bool图不是头文件的复制品。只画与当前讨论有关的属性和方法讨论支付协作时订单数据库字段没有必要全部出现讨论内存布局时才需要更具体的信息。细节越多不等于越专业恰到好处才便于维护。3.2 六种关系要分组理解依赖、关联、聚合、组合描述对象怎样使用或构成彼此实现、继承描述类型合同。两组关系关注的维度不同不适合硬排成一条绝对的“强度顺序”。先看一张包含六种关系的图实现继承组合聚合关联依赖«interface»IDrawdraw() : const«abstract»ShapeCircledraw() : constCanvasadd(shape)ShapeGroupadd(shape)Editorpreview(shape)依赖Dependency表示临时使用。Editor::preview()通过参数接触Shape调用结束后不保存它。Mermaid 使用虚线箭头..。关联Association表示较长期的认识关系。Editor保存Canvas的引用可以持续调用它但不负责销毁它。使用实线箭头--。关联是否拥有对象需要结合角色说明或代码判断不能只看“成员指针”四个字。聚合Aggregation表示整体收纳可以独立存在、也可能被共享的部分。ShapeGroup引用外部形状组被销毁后形状仍可存在。使用空心菱形o--。很多团队很少区分普通关联和聚合如果空心菱形不能增加信息使用关联并写清所有权也完全可以。组合Composition表示更强的整体—部分语义部分由一个整体独占生命周期通常由整体控制。Canvas独占形状画布销毁时形状一起释放。使用实心菱形*--。组合强调的是模型语义不是看到unique_ptr就必须画菱形。实现Realization表示类型兑现接口合同使用虚线三角..|。在 C 的运行时多态中常对应继承纯虚抽象基类并实现其操作。继承或泛化Generalization表示更具体的类型继承一般类型使用实线三角--|。它不仅复用代码更承诺派生类满足基类的行为合同因此必须符合里氏替换原则。可以用下表快速回忆但要记住 UML 关系和 C 写法不是机械的一一映射分组关系Mermaid常见 C 表达关键问题使用依赖A .. B参数、局部变量是否只临时使用使用关联A -- B成员引用或指针是否长期知道对方整体聚合A o-- B非拥有引用集合部分能否独立或共享整体组合A *-- B值成员、独占指针是否独占并控制生命周期类型实现A … I实现抽象接口类型继承A – Bpublic继承3.3 把图翻译成 C下面的代码与前图逐项对应#includememory#includeutility#includevectorstructIDraw{virtual~IDraw()default;virtualvoiddraw()const0;};// 实现 IDraw仍保留为抽象形状角色classShape:publicIDraw{public:~Shape()overridedefault;};classCirclefinal:publicShape{public:voiddraw()constoverride;};// 组合独占形状的生命周期classCanvas{public:voidadd(std::unique_ptrShapeshape){shapes_.push_back(std::move(shape));}private:std::vectorstd::unique_ptrShapeshapes_;};// 聚合只观察外部形状不拥有classShapeGroup{public:voidadd(Shapeshape){parts_.push_back(shape);}private:std::vectorShape*parts_;};classEditor{public:explicitEditor(Canvascanvas):canvas_(canvas){}// 依赖只在调用期间使用 shapevoidpreview(constShapeshape);private:Canvascanvas_;// 关联长期引用不拥有};代码还能提醒我们图上看不到的风险ShapeGroup中的裸指针不拥有对象外部必须保证形状比组活得更久。UML 说明结构语义C 类型进一步表达所有权两者结合才完整。4. 序列图沿时间轴理解协作类图像合影序列图像剧本。参与者横向排列时间从上到下流动一条消息越靠上发生得越早。序列图的常用元素包括参与者与生命线、表示执行期间的激活条、同步消息、异步消息和返回消息。alt表示互斥分支opt表示可选步骤loop表示循环par表示可并行的片段。下面用观察者式事件通知描述订单支付后的交互ObserverEventBusClientObserverEventBusClientloop[遍历当前订阅快照]publish(OrderPaid)1onOrderPaid(order)2handled3done4这张图表达的是客户端只发布一次事件事件总线遍历订阅者订阅者彼此不认识。图中选择了同步通知并等待返回但观察者模式本身不保证同步、异步或固定顺序如果这些性质影响业务就应在图旁明确写出并由接口和测试兑现。画序列图时还要控制层级。讨论领域协作就不要混入日志库内部函数和数据库驱动调用排查性能瓶颈时则可以画一张更细的图。每张图只回答一个层级的问题读者才不会在箭头森林里迷路。5. 状态机图把生命周期写成合同同一个操作在不同状态下可能产生完全不同的结果待支付订单可以取消已发货订单通常不能直接取消。状态机图把允许的状态、触发事件和转移条件集中在一处。常用元素有状态、事件、转移、守卫和动作。[*]表示初始或终止伪状态转移标签可按下面的形式阅读事件 [守卫条件] / 转移动作创建订单支付成功 / 确认库存支付超时 / 释放预留商家发货申请退款 [允许退款] / 原路退回确认收货 / 完成结算待支付已支付已取消已发货已完成阅读“申请退款”这条线时先发生事件再检查[允许退款]只有守卫为真才执行/原路退回并进入已取消状态。图上没有箭头的转移默认不被允许例如已完成订单不能重新回到待支付。状态图适合确认规则是否完整也能生成测试用例每条合法箭头至少对应一个成功场景每个关键状态再补非法事件场景。第 21 篇状态模式会讨论怎样把状态相关行为组织进代码但不会机械规定“一种状态必须有一个类”简单状态机使用枚举和表驱动也可能更合适。6. 用例图与活动图认识即可用例图站在系统边界外观察需求。小人代表参与者椭圆代表系统提供的能力include表示必然包含的公共用例extend表示在特定条件下发生的扩展。它适合回答“谁使用系统、获得什么价值”不适合描述类之间如何调用。Mermaid 没有原生用例图语法需要时可以用flowchart近似或选择专门工具。活动图关注步骤、判断、循环和并行适合业务流程与算法过程。Mermaid 同样可以用流程图表达是否下单请求库存充足?预留库存提示缺货创建订单结束当问题是“流程怎么走”时活动图通常比序列图简洁当问题变成“这些步骤分别由哪个对象执行”再切换到序列图。7. 画图最常见的四个误区把所有细节都画进去。图如果和头文件一样长就失去了抽象价值。先写下图要回答的问题只保留支撑答案的元素。图与代码长期不同步。Mermaid 跟随 Markdown 进入版本库只是降低维护门槛不会自动同步。修改关键依赖、所有权或消息顺序时应把相关图纳入同一次评审。用错视角。类图擅长静态关系却不适合表达调用先后活动图擅长流程却不一定说明对象职责。图越画越绕时先检查是否选错了类型。把符号正确当成设计正确。实心菱形画得再标准也不能证明生命周期安全继承箭头方向正确也不能证明派生类符合 LSP。UML 负责暴露设计原则、代码和测试负责验证设计。8. 怎么选图一张决策表想回答的问题优先选择典型场景类型之间是什么结构关系类图适配器、装饰器、桥接运行时谁先调用谁序列图观察者、命令、责任链对象有哪些状态和合法转移状态机图订单、连接、状态模式业务步骤怎样分支或并行活动图审批、下单、数据处理系统为哪些角色提供能力用例图需求范围与系统边界如果一个问题需要三种图才能说清就分别画三张小图不要强行塞进一张“大而全”的总图。读图时也遵循同样顺序先确认这张图想回答什么再看角色最后沿关系或时间逐步阅读。本篇小结类图表达静态结构序列图表达交互顺序状态机图表达生命周期规则。依赖、关联、聚合、组合与实现、继承属于不同观察维度不应机械排成一条强弱链。UML 说明设计语义C 类型进一步表达多态和所有权两者需要相互一致。图应围绕一个问题保持精简并与代码一起维护。下一篇进入第一个正式模式单例模式及其线程安全、生命周期与测试边界。
