ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Thinking in UML配套光盘模型解析与文本化迁移

Thinking in UML配套光盘模型解析与文本化迁移

简介:《大象-Thinking in UML》配套光盘面向UML建模初学者与软件设计人员,围绕书中案例提供可直接查阅的彩色插图、Rose建模原文件及无需安装工具的HTML版本,帮助读者把书中的图例、建模过程与系统分析思路对应起来。压缩包共1903个文件,约9.25MB,以htm网页、tif插图、png/jpg图片和cnt目录文件为主,另含mdl模型、pdf辅助读物、jar及少量txt说明:网页与图片适合浏览图例,mdl文件便于在Rational Rose 2002及以上版本中复盘建模组织方式。已有573人学习下载。光盘中的建模示例覆盖“第三部分 进阶篇”实例的完整建模过程,HTML版可通过浏览器直接阅读,并附带Java虚拟机安装文件以解决applet显示问题;OO系统分析员之路PDF则补充了作者博客系列文章,适合作为UML课程学习、软件建模实践与系统分析员进阶的辅助材料。

1. 光盘里到底有什么:三类文件与两种打开方式

搜「《大象-Thinking in UML》的配套光盘下载」的人,多半是二手书里夹着一张盘、或者只拿到书没拿到盘。这张盘是书里每张图的原始模型文件、一套当年随书附带的建模工具安装包、按章拆开的课件和示例代码。它要解决的就是两件事:书上的图照着画不出来,图画出来了不知道怎么用到项目里。适合正在做需求分析、准备系统分析设计类考试、或者要给祖传系统补一份模型存档的人。打开方式其实只有两种:装工具打开原生模型文件,或者用文本化工具重建一份能长期维护的版本,后者我后面会重点讲。

2. 从用例到类图:把示例模型拆成可复用的分析套路

光盘里的模型文件不是拿来欣赏的,是一套可以照着推演的分析链路:用例图定边界,活动图理流程,时序图验证交互,类图收口到静态结构。多数人打开模型第一反应是「图挺全」,然后关掉。真正有用的读法是反过来——先挑一个自己手头业务里最像的子系统,把光盘里对应的那几张图当成草稿,一步步改写成自己的。下面按建模顺序拆三段,每段都给可抄的结构和判定标准。

2.1 用例模型的粒度:参与者、用例与系统边界怎么划

初学者最容易把用例画成功能清单。「登录」「查询」「新增」「删除」一字排开,看着整齐,实际上颗粒度全错。用例的判定标准只有一条:它是不是一个参与者为了达成某个可观测目标而触发的完整行为。登录本身通常不是目标,它是达成目标的前置条件,所以更合理的做法是把登录抽成被包含的公共用例,而不是每个业务用例都连一条线到「用户」。

参与者也一样,参与者是角色不是人。同一个人在不同场景下可能是三个参与者,比如「下单人」「审批人」「对账人」,这三者的权限和关注点完全不同,画成一个「员工」等于什么都没说。系统边界框则是很多人直接省略的东西:它决定了哪些用例属于你这次要建的系统,哪些是外部系统要背的锅。边界画错,后面所有的接口设计都要返工,这是血泪经验。

判定项合格写法常见错误写法
参与者按角色职责命名,如「库存管理员」按岗位或人名命名
用例名动宾结构,如「提交退货申请」名词,如「退货模块」
粒度一次会话内可完成、有明确结果把 CRUD 各列一条
关系include 表必做步骤,extend 表可选分支用 extend 表达「顺序执行」
边界明确本系统与外部系统的分界把所有相关方都圈进框内

读光盘里那套用例模型时,建议先数一下单个子系统的用例数量。经验值是一个子系统的主用例控制在 7 到 12 个之间,超过 20 个基本可以判断是没有做抽象。这时候不要急着改图,先把每个用例的「触发事件」「参与角色」「成功后的可观测结果」三列写成表格,写完再回头看哪些可以合并、哪些其实是另一个用例的 include 分支。

再看用例之间的关系。include 是「必然发生」的复用,比如「提交订单」必然要「校验库存」,方向是从基用例指向被包含用例;extend 是「条件发生」的扩展,比如「提交订单」在特定条件下触发「使用优惠券」,方向反过来,从扩展用例指向基用例。这个方向在很多工具里画错不会报错,但评审时一眼就能看出来,属于典型的低级翻车点。

最后是场景描述。光盘里的模型大多配了用例规约,这是比图更有价值的部分。规约里通常有前置条件、后置条件、基本流、备选流四段,读的时候重点看备选流——基本流谁都会写,备选流才暴露业务复杂度。把三个最复杂的备选流抄出来,逐个对应到时序图上,如果时序图里找不到这条分支,说明这套模型是「画完就扔」的,你自己用的时候必须补齐。

2.2 类图的三类关系落地:关联、聚合、组合的判定标准

类图是整套模型里最容易画、也最容易画废的一张。语法就那么几种线,难的是判断两个类之间到底该用哪种。判定顺序我一般这样走:先问「有没有结构上的长期引用」,没有就是依赖;有引用再问「整体销毁时部分是否跟着销毁」,跟着就是组合,不跟就是聚合;两者都不是纯粹的生命周期绑定,就是普通关联。

下面这段是可以直接跑的 PlantUML,把订单域的典型结构写全了,包括多重性、导航性和接口实现:

@startuml interface Payable { +pay(amount: Money): boolean } class Order { -orderId: String -items: List<OrderItem> +total(): Money } class OrderItem { -sku: String -qty: int -price: Money } class Customer { -customerId: String } class Address { -detail: String } ' 组合:订单消失,明细必须消失 Order *-- "1..*" OrderItem ' 聚合:客户与订单是弱拥有,订单可独立存在 Customer o-- "0..*" Order ' 普通关联:地址可被多个对象复用 Customer --> "1" Address ' 实现关系 Order ..|> Payable @enduml

代码里的关键点在三处。*--是组合,箭头在整体一侧,菱形实心,语义是「部分随整体生命周期结束」,订单删了明细没有独立存在的意义,所以用组合。o--是聚合,空心菱形,语义是「整体管理部分但部分可独立存在」,客户注销了历史订单还得留着做审计,所以用聚合。..|>是实现,接口在右侧,这条线决定了后面代码里是implements还是继承。

多重性标注也经常被写成摆设。1..*的意思不是「至少一个」,而是「在这个关联上,一个 Order 实例在任意时刻都必须对应至少一个 OrderItem」,这意味着 0 元订单在模型层就是不合法状态。如果你的业务允许空订单草稿,这里要么改成0..*,要么把草稿单独建一个类,别在多重性上打马虎眼。

关联类是被忽略最多的一种结构。当两个类之间的关联本身带属性时,比如「学生」和「课程」之间的「选课」有成绩、有学期,正确做法是把选课提成关联类,而不是把成绩硬塞进学生类。光盘里的示例通常会在这种地方做示范,对着看一遍胜过读三章理论。

写代码的时候,这些关系直接映射成字段类型。组合对应强持有,构造函数里初始化;聚合对应可注入的集合;普通关联对应一个引用,通常由外部传入。类图画对了,代码骨架基本就是机械翻译;类图画错了,后面靠注释解释关系,三个月后自己都看不懂。这也是我建议先把类图收敛到 15 个核心类以内再动手写代码的原因。

2.3 时序图与活动图的互补:控制流和对象交互各归各

很多团队只画一种图,然后指望它包打天下。活动图擅长表达「流程走到哪一步、什么条件下分叉」,时序图擅长表达「这一步是谁调谁、消息按什么顺序发」。两者不是替代关系,是视图切换:活动图给业务方看流程,时序图给开发看接口。

一个实用的做法是先画活动图定泳道。泳道按系统或角色划分,每个泳道里的动作必须能落到某个具体的类或服务上。如果某个动作在泳道里找不到归属,说明系统边界没划清,这时候回头改用例图比硬画时序图有效得多。活动图里的分叉和汇合要成对出现,只分不合是常见错误,模型校验工具不一定报,但评审时会被追问「这个分支走完回哪里」。

时序图这边,重点看三样东西:生命线的顺序、消息的同步异步、组合片段的条件。生命线从左到右的顺序应该反映调用层级,调用方在左、被调方在右,跨层调用尽量少出现。同步消息用实心箭头,异步消息用开放箭头,这个区别在纸面上不明显,但落到代码里就是「阻塞等待返回」和「发出去就不管」两种完全不同的模型,混用会导致超时设计全错。

组合片段里 alt 和 opt 别乱用。alt 是互斥分支,必须覆盖所有可能;opt 是可选执行,条件不满足就跳过。还有 loop 片段,要标清楚是固定次数还是集合遍历,这直接影响代码里是 for 循环还是 while。光盘里有些老图用的是 UML 1.x 的表示法,比如把条件直接写在消息上而不是用片段框,看的时候要能自动翻译,别照着抄进新文档里。

验证方法是把时序图倒着走一遍:从最后一个返回消息往前推,每一步问「这个调用的发起者在当前上下文里是否可达」。走不通的地方就是设计漏洞,通常是某个对象被凭空创建,或者某条消息跨了两个不该直接通信的层。这个动作花不了十分钟,能省掉后面一轮返工。

3. 光盘里的模型文件怎么打开:格式、工具与转换

光盘里的模型文件是这套资源最硬的部分,也是最容易被卡住的地方。文件打不开,后面全白搭。这一章讲清楚三件事:文件是什么格式、用什么打开、打不开时怎么无损转换。前置提醒一句,别装完工具就双击文件,老版本格式和新版工具之间的兼容性比想象中差。

3.1 六种模型文件格式与它们的兼容边界

模型文件本质上是 XML 或者私有二进制,不同工具各玩各的。把目录里所有非文档、非图片的文件按扩展名归一下类,基本就这几类:

扩展名性质打开方式兼容风险
.mdl私有二进制对应商业建模工具老版本新版普遍不再支持,需中转
.eap内嵌数据库对应商业建模工具版本升级后可能要求迁移
.uml/.diXML 文本开源建模工具跨大版本可能解析失败
.zargo压缩包,内含 XML开源建模工具需先解压看内部结构
.xmiOMG 标准交换格式多数工具可导入最稳的中转格式
.emf/.ecore元模型文件Eclipse 系建模框架依赖插件,单独打不开

判断优先级很明确:先找.xmi,有就直接导入;没有就找.uml、.zargo这类 XML 系;只剩私有二进制时,才考虑装对应版本的工具或者用官方迁移功能。私有二进制不是不能读,是读出来之后改不动,只能当只读存档,这一点要提前有心理预期。

还有一类文件容易被忽略:模型导出图片。很多光盘会把每张图另存成 png 或 emf 矢量图,这些不是模型,但有独立价值——在没法打开原生文件的环境里,它们至少能让你看清图的完整结构,用来对照重建。别小看这一步,我见过太多人卡在「文件打不开就放弃了」,其实图片版本就在隔壁目录。

3.2 用 XMI 做中转:跨工具迁移的可复现步骤

XMI 是这套流程里唯一的通用语言。它能带走类、属性、关系、多重性这些结构信息,带不走的是图形布局和部分样式,所以转换后要重新排版,这是必须接受的代价。迁移步骤固定成四步,照着走就行。

第一步,在能打开原文件的工具里执行导出。导出选项里如果有 XMI 版本选择,选 2.1 或者 2.4,别选 1.x,老版本对泛型、接口这些支持不全。勾选「导出所有图」和「包含类型信息」,这两个选项决定了导出的完整度。

第二步,检查导出文件的结构。XMI 是 XML,直接打开看根节点和命名空间:

# 看一眼根节点和命名空间,确认版本 head -n 20 model.xmi # 统计类、关联、属性的数量,和原模型对照 grep -c "uml:Class" model.xmi grep -c "uml:Association" model.xmi grep -c "uml:Property" model.xmi

三个数字必须和原工具的模型统计对得上,对不上说明导出丢了东西,通常是没勾选完整导出,或者原模型里存在工具私有的扩展元素。数量对得上再往下走,这一步省不得,我吃过亏。

第三步,在目标工具里导入。导入后先别急着看图形,切到模型树看元素是否齐全,特别检查三处:泛化关系的方向、接口实现的虚线、关联类的属性。这三处是 XMI 转换里丢失率最高的地方,图形布局反而是次要的。

第四步,导出为可读文本做二次校验。如果你的目标工具支持脚本或命令行,把模型再导一次到平铺文本,和第一步的统计做 diff。没有差异就说明转换干净了,有差异就定位到具体元素手工补。

# 用脚本做一次结构对比,只比元素类型和名称 import xml.etree.ElementTree as ET def collect(path): tree = ET.parse(path) names = set() for el in tree.iter(): tag = el.tag.split('}')[-1] if tag in ('Class', 'Interface', 'Association', 'Property'): name = el.get('name') if name: names.add(f"{tag}:{name}") return names src = collect('model.xmi') dst = collect('rebuilt.xmi') print('丢失:', sorted(src - dst)[:20]) print('多出:', sorted(dst - src)[:20])

这段脚本抓的是元素类型加名称的组合,够用来做粗粒度校验。el.tag.split('}')[-1]是为了剥掉命名空间前缀,不同工具导出的前缀不一样,直接比字符串会全量误报。输出的两个集合如果都为空,基本可以认定迁移完整。

3.3 只读存档与版本管理:模型文件怎么进 Git

模型文件能不能进版本库,取决于它是文本还是二进制。.xmi、.uml、.zargo内部是文本,进 Git 后能 diff、能合并,虽然合并冲突解决起来也头疼,但至少看得见改了哪几行。.mdl、.eap是二进制或者内嵌数据库,每次提交都是一个全新的 blob,仓库体积会迅速膨胀,而且冲突时完全没法处理。

我的做法是分两条线:原始文件打成一个只读压缩包,放在单独的资产目录,不进主分支;转换后的 XMI 或文本模型进主仓库,跟着代码一起演进。这样既保住了原始存档,日常协作又不受二进制文件拖累。

# 原始模型归档,不进版本历史 mkdir -p archive && tar -czf archive/models-original-$(date +%Y%m%d).tar.gz ./disc/Models # 文本模型纳入版本管理 git add models/*.xmi models/*.puml git commit -m "chore: 导入光盘示例模型并转为 XMI"

还有个细节:.gitattributes里给模型文件标记为文本类型,避免换行符自动转换把 XML 弄坏:

*.xmi text eol=lf *.puml text eol=lf *.mdl -text

-text是告诉 Git 别对二进制做任何处理,这行如果漏了,在某些系统上检出后文件直接损坏,属于修起来很烦的玄学问题。

4. 配套课件与示例源码的使用顺序:先跑通再读

光盘里的课件和源码,最大的浪费方式是从第一页 PPT 看到最后一页。正确顺序是反过来的:先让示例代码跑起来,再带着运行结果回去看模型和课件。跑通的那一瞬间,模型里的抽象概念会突然落地,这比读十遍图例有效。下面给一条可执行的路径。

4.1 目录结构与阅读路径

先花五分钟把顶层目录扫一遍,按性质分类,别急着深入任何子目录:

目录特征内容性质使用顺序
名字含 model / diagram原生模型文件第二顺位,用于对照结构
名字含 doc / ppt / 讲义课件与说明第三顺位,按需查阅
名字含 src / code / sample示例源码第一顺位,先跑通
名字含 tool / setup / install安装包按需,能不用就不用
根目录散落的图片模型导出图随时查阅,替代打不开的模型

第一顺位永远是源码。找一个带主入口的工程,确认构建方式和依赖。老项目常见的是 Maven 或 Ant,配置文件里的仓库地址多半已经失效,这时候不要死磕,把依赖项手工降到本地能找到的稳定版本,先编译通过再说。

第二顺位才是模型文件。代码跑通之后,回头打开对应的类图和时序图,逐个对照「这个类在代码里是哪个文件」「这条消息对应哪次方法调用」。这种对照做三遍,比看三十张图都有用,因为你是在用代码验证模型,而不是用模型猜代码。

课件放最后。PPT 的价值集中在两张:一张是章节开头的概念对比表,一张是结尾的图例汇总。中间大段的文字页可以跳过,那些内容书上都有。带着问题去翻课件,效率高得多。

4.2 把模型映射成代码骨架

示例代码跑通之后,做一件更有价值的事:自己动手把模型翻译成骨架,再和光盘里的源码对比差异。差异点就是你对建模理解不到位的地方。

以类图为例,翻译规则并不复杂:

from dataclasses import dataclass, field from typing import List @dataclass class OrderItem: sku: str qty: int price: float @dataclass class Order: order_id: str # 组合关系:容器初始化时创建,随整体销毁 items: List[OrderItem] = field(default_factory=list) # 聚合关系:外部注入,可为空 customer_id: str = "" def total(self) -> float: return sum(i.qty * i.price for i in self.items)

这里两个字段的写法差异,直接对应类图上组合和聚合的区别。field(default_factory=list)是组合的代码形态,容器自己负责创建和管理;customer_id用普通字段,值由外部传入,是聚合的形态。写的时候如果两个都用可空引用,说明类图上的关系判定其实没想清楚。

翻译时的参数说明也要落到注释里。多重性1..*对应「列表不允许为空」,代码里要么在构造函数校验,要么在类型上体现;多重性0..1对应可选类型,别用空字符串冒充 null,这两种在后续序列化时行为完全不同。

对比差异时重点看三处:接口的粒度、聚合的边界、异常路径。接口粒度看示例代码是不是把多个小方法合成一个大服务,合成过度说明抽象层级没找准;聚合边界看对象之间是不是互相持有引用,互相持有是典型的循环依赖;异常路径看备选流有没有对应的错误码或异常类,没有就说明模型只画了 happy path。

4.3 课件里的图怎么用:只取对比和结论

课件里的图有两类值得留:一类是同一概念的不同表达对比,比如「关联 vs 依赖」并排放两张图,这种对比能快速校准判断标准;另一类是建模步骤的流程图,告诉你先画什么后画什么,顺序错了会返工。

不值得留的是大段截图和软件界面说明。界面会变,截图会过时,照着截图记菜单项纯属浪费时间。真正需要记的是判定规则,比如「什么情况下用组合不用聚合」,这种规则十年不变。

我一般会把课件里所有带「对比」「区别」「原则」字样的页面截图存到一个文件夹,命名成「判定卡」,做模型评审的时候直接翻,比查文档快。这个习惯是从一次评审被问到「你为什么用聚合不用组合」答不上来之后养成的。

5. 避坑与常见问题:五个翻车现场与排查顺序

光盘类资源的问题高度集中,翻来覆去就那几类。这一章按「现象 → 原因 → 解决」把最常见的五个坑写清楚,遇到问题先按这个顺序排查,能省掉大量试错时间。

5.1 文件层:打不开、乱码、安装包失效

现象一:双击模型文件,工具要么没反应,要么弹「文件格式不受支持」。原因基本是版本断层,老版本工具生成的私有格式,新版直接弃用了,或者反过来,用老工具打开新文件。解决办法不是反复装工具,而是先找同目录下有没有.xmi或导出图片;没有的话,装一个中间版本的工具做一次另存,再升级到目标版本,两级跳比一步到位成功率高得多。

现象二:在非 Windows 环境挂载光盘镜像后,文件名全是乱码,中文目录变成问号。原因是光盘文件系统用的编码和当前系统默认编码不一致,常见于早期制作的盘。解决办法是挂载时显式指定编码,或者干脆用解压工具直接抽取:

# 挂载时指定编码,按实际情况换 GBK 或 UTF-8 mount -o loop,iocharset=gbk disc.iso /mnt/disc # 或者不解挂载,直接解压后批量转码文件名 7z x disc.iso -o./disc_raw find ./disc_raw -depth -name "*" -execdir convmv -f gbk -t utf8 --notest {} +

convmv那一行的-depth参数很关键,必须从最深层开始改,否则父目录改名后子路径全失效。这类问题修一次就够,把转码后的目录重新打包存档,后面不再碰原盘。

现象三:安装包运行到一半报错退出,或者装完提示需要许可、只能读不能存。原因是随书附带的工具多为当年的试用版或教育版,授权机制在现代系统上已经失效,加上安装程序本身可能是 16 位组件。解决办法很简单:别装。光盘里的模型文件用导出图片看,或者用文本化工具重建,安装包只作为最后手段。

5.2 内容层:版本不一致与示例不全

现象四:对照书上的图打开模型,发现类少了几个、关系方向反了。原因是光盘对应的印次和你手上这本书不一致,书在重印时改过图,盘没跟着更新;也可能是光盘里的模型是「简化版」,只保留了讲解用的核心元素。解决办法是以书为准,把模型当骨架而不是标准答案。具体做法是打开模型后先做一次元素清点,缺的部分照书补,补的过程中顺手把命名统一成项目里的规范,一举两得。

现象五:示例代码编译不过,报找不到依赖或者编码错误。原因通常是构建脚本里写死了内网仓库地址,以及源码文件用的是 GBK 编码。解决办法分两步,先把依赖版本手工降到本地仓库已有的稳定版本,再统一转成 UTF-8:

# 批量转编码,先备份 cp -r src src_bak find src -type f \( -name "*.java" -o -name "*.properties" \) \ -exec sh -c 'iconv -f gbk -t utf-8 "$1" > "$1.tmp" && mv "$1.tmp" "$1"' _ {} \;

iconv遇到无法转换的字符会中断,加-c可以跳过非法字符但可能丢内容,所以务必先备份。转完再编译,报错信息会清晰很多,剩下的基本就是缺依赖,逐个补即可。

5.3 排查顺序:从外到内,别一上来就改模型

遇到问题时的排查顺序比具体技巧更重要。我的顺序是:先确认文件能不能读(编码、格式、完整性),再确认工具能不能开(版本、许可、依赖),然后确认内容对不对(元素数量、关系方向),最后才动模型改结构。这个顺序不能颠倒,因为前面的问题会伪装成后面的问题。文件编码错了,表现出来的可能是「类名乱码导致关系匹配失败」,你会误以为是模型结构有问题,改半天白费。

还有一个通用技巧:任何一次打开模型之前,先把目录结构和文件清单导出成文本存档,包括文件大小和修改时间。这个清单在后续对比「我到底改了什么」时非常有用,尤其是模型文件本身没法 diff 的时候。

6. 进阶:把光盘模型转成可 diff 的文本模型

原生模型文件最大的问题是不可维护:改一处没法 review,合并冲突没法解决,十年后工具没了文件就死了。我的做法是把核心模型转成 PlantUML 文本,和代码放同一个仓库,改动走同一条评审流程。

转换不必全自动。类图、用例图这类结构图用脚本从 XMI 批量生成骨架,时序图、活动图手工写,因为这两类图的语义密度高,自动生成的结果往往还不如手写快。下面这个脚本从 XMI 里抽出类名和关联,输出 PlantUML 骨架:

import xml.etree.ElementTree as ET NS = {'uml': 'http://www.omg.org/spec/UML/20131001', 'xmi': 'http://www.omg.org/spec/XMI/20131001'} def to_puml(path): root = ET.parse(path).getroot() lines = ['@startuml', 'skinparam classAttributeIconSize 0'] ids = {} # 第一遍:收集类与接口 for el in root.iter(): tag = el.tag.split('}')[-1] if tag in ('Class', 'Interface'): xid = el.get('{http://www.omg.org/spec/XMI/20131001}id') name = el.get('name', 'Unnamed') ids[xid] = name keyword = 'class' if tag == 'Class' else 'interface' lines.append(f'{keyword} {name}') # 第二遍:收集关联的两端 for el in root.iter(): if el.tag.split('}')[-1] != 'Association': continue ends = [e for e in el if e.tag.split('}')[-1] == 'ownedEnd'] if len(ends) == 2: a = ids.get(ends[0].get('type'), '?') b = ids.get(ends[1].get('type'), '?') lines.append(f'{a} --> {b}') lines.append('@enduml') return '\n'.join(lines) print(to_puml('model.xmi'))

脚本里两遍遍历是必要的:关联的两端只存了类型 id,必须先建立 id 到名字的映射才能输出。命名空间那两行长串如果和目标文件对不上,就直接用tag.split('}')[-1]的方式绕过,兼容性更好。输出的结果是骨架,多重性、组合聚合这些需要手工补,但类名和拓扑关系已经省了八成工作量。

文本模型的真正价值在评审。改一个关系就是改一行文本,diff 清清楚楚,谁在什么时候把聚合改成了组合一目了然。再挂一个流水线钩子,提交时自动渲染成 SVG 附在构建产物里,图永远和代码同步,不会出现「文档里的图是三年前的」这种局面。

从那以后我每次拿到这类光盘资源,第一件事都是先做一份文本化转换再动手看内容,哪怕只转核心的十几张图。因为原生文件是黑匣子,而文本模型是后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进