ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

UML配套光盘模型复用:XMI迁移、旧版兼容与验收

UML配套光盘模型复用:XMI迁移、旧版兼容与验收

简介:《大象-Thinking in UML》配套光盘面向UML初学者、软件建模实践者及高校相关课程学习者,用于配合书中案例理解面向对象分析与设计流程。压缩包共1903个文件、约9.25MB,以htm网页、tif/png/jpg等插图、cnt目录文件为主,并含jar、pdf、mdl与db等配套文件,收录彩色插图、Rose建模原文件、HTML版建模示例、Java虚拟机及《OO系统分析员之路》PDF。读者可用Rose原文件还原建模全过程,观察各阶段模型的组织方式;未装Rose时可通过HTML版在浏览器查阅同一示例,并借助Java虚拟机解决显示问题。彩色插图按书中编号命名,便于对照细节;PDF文章则补充面向对象系统分析员的学习路线。已有573人学习下载,适合希望把UML理论落到建模实践中的读者参考。

1. 《大象-Thinking in UML》配套光盘下载之后:先分清它是资料盘还是安装盘

《大象-Thinking in UML》配套光盘下载下来之后,很多人的第一反应是双击根目录那个自动播放菜单,然后对着一屏十几年前风格的界面发呆。这份资源的本质不是软件安装包,而是一整本 UML 教材的副产物:全书示例的模型工程文件、按章节拆开的电子课件、案例素材图,可能还夹着建模工具的试用安装包和几段建库脚本。它解决的是“不想从空白画布开始画图”这个问题——用例图、类图、时序图、活动图都已有成品,把里面的名词换成自己项目的,比从头画省太多事。适合三类人:正在系统学 UML 建模的学生、要给需求文档和设计文档配图的后端工程师、以及需要一套完整建模案例撑起课程设计的人。先把“这是资料盘不是安装盘”这件事想明白,后面的操作才不会走偏。

2. 拆开光盘目录:模型文件、课件与工具包的三层结构

光盘镜像常见两种形态,一种是单个 ISO,挂载或解压后得到一棵目录树;另一种是别人转存过的散装文件夹,里面套着“新建文件夹(2)”这种命名。不管哪种,拿到手第一件事都不是打开某个模型文件,而是把目录结构摸清楚。这类配套盘一般按章节编号组织,ch01到ch12各一份,每种资源一个平行子目录:模型工程、幻灯片、案例源码、工具安装包、说明文件。老光盘的命名相当随意,中文路径、空格、全角括号混着来,有的还把 Readme 存成 GBK 编码。先做体检再开图,是省时间的做法。

2.1 先给整张盘做一次文件指纹体检

体检要解决三个问题:盘里到底有多少文件、哪些是重复转存的、解压有没有缺文件。哈希值就是那个“后悔药”——后面用建模工具打开再保存,文件被工具悄悄升级成新格式时,你还能拿出原始哈希对照,确认哪些被动过。

import csv, hashlib, os, sys def sha256_of(path, chunk=1024 * 1024): """分块读取计算哈希,避免一次性把大文件读进内存""" h = hashlib.sha256() with open(path, "rb") as f: for block in iter(lambda: f.read(chunk), b""): h.update(block) return h.hexdigest() rows = [] root = sys.argv[1] # 光盘挂载点,或解压后的根目录 for dirpath, _, files in os.walk(root): for name in files: full = os.path.join(dirpath, name) try: size = os.path.getsize(full) except OSError: continue # 坏扇区或权限问题,跳过而不是中断 rows.append({ "rel_path": os.path.relpath(full, root), "ext": os.path.splitext(name)[1].lower(), "size_kb": round(size / 1024, 1), "sha256": sha256_of(full) if size < 200 * 1024 * 1024 else "", }) with open("manifest.csv", "w", newline="", encoding="utf-8-sig") as f: w = csv.DictWriter(f, fieldnames=["rel_path", "ext", "size_kb", "sha256"]) w.writeheader() w.writerows(rows) print(len(rows), "files indexed")

几个参数值得说一下。chunk取 1MB 是磁盘 IO 和内存占用的折中,光盘上全是几 MB 的模型文件,再大没有意义。size < 200 * 1024 * 1024这个阈值是给工具安装包留的余地——超过 200MB 的基本是安装程序,不需要算哈希,省一半时间。写文件时用utf-8-sig而不是utf-8,是因为这份 CSV 你大概率要用表格软件打开核对,带 BOM 才不会让中文路径变成乱码。rel_path用相对路径而不是绝对路径,是为了后面把这份清单直接提交进版本库,换台机器也能复现。

跑完之后先看总行数。如果解压出来的文件数和光盘说明里写的对不上,八成是解压中断或者镜像本身有问题,这时候别急着往下走,先重新挂载。

2.2 后缀对照表:哪个文件该用哪个工具接

模型工程文件是这张盘里最值钱的部分,也是最容易打不开的部分。老教材配套的工程文件后缀五花八门,同一个知识点在不同章节可能用了不同工具存。先把后缀和工具对上号,再决定用哪条路打开。

后缀常见来源能否直接打开处理办法
.mdl早期商业建模工具的原生工程需要同族工具,且版本不能差太远先只读备份,再考虑导出交换格式
.vpp另一款商业建模工具工程高版本工具通常能读低版本用新版打开后立刻另存,别在原文件上保存
.zargo开源建模器工程,本质是压缩包需要同族工具直接解压,内部一般有 XMI
.oom老一代对象建模工具现代系统上基本打不开只当参考,靠截图对照重画
.xmi通用模型交换格式多数建模器可导入首选的中转格式
.uml文本化的模型描述纯文本编辑器就能读直接进版本库,最好管
.drawio/.vsdx手绘示意图、文档内嵌图通用绘图工具当素材用,不做结构复用
.ppt/.doc电子课件、实验指导办公套件注意缺字体导致的排版错位
.sql案例库建库脚本数据库客户端建库后跑一遍,验证案例数据完整

一个容易忽略的点:.zargo这类后缀看着像私有格式,其实就是个 zip,把后缀改成.zip解压,里面经常直接躺着 XMI 文件。这一步能把“需要装一个十几年前的工具”变成“用现成工具导入 XMI”。反过来,.mdl是二进制私有格式,除了原厂工具没有可靠解法,别指望文本编辑器能救。

2.3 按扩展名做一份体量表,判断“这盘值不值得留”

光盘动不动几百 MB,但真正有复用价值的往往只有几 MB。做个体量分布,能帮你决定哪些目录进版本库、哪些直接丢掉。

import collections, csv rows = list(csv.DictReader(open("manifest.csv", encoding="utf-8-sig"))) count = collections.Counter() weight = collections.Counter() for r in rows: ext = r["ext"] or "(no ext)" count[ext] += 1 weight[ext] += float(r["size_kb"] or 0) for ext, n in count.most_common(): print(f"{ext:10s} {n:5d} 个 {weight[ext] / 1024:8.1f} MB") # 同一份文件被拷了多份:按哈希分组,超过一条就是重复 dup = collections.defaultdict(list) for r in rows: if r["sha256"]: dup[r["sha256"]].append(r["rel_path"]) for h, paths in dup.items(): if len(paths) > 1: print("重复:", paths)

这段的输出通常长这样:.bmp或.jpg数量最多、体积最大,.mdl、.xmi数量少但排在价值第一位,工具安装包单独占一大块。我的习惯是把模型工程、.sql、.uml单独抽到一个keep/目录,课件按章节留,安装包和示例图片全部不进库——图片占了八成体积,真正有用的信息在图里,不在文件本身。

重复检测也要看一眼。同一份ch05.mdl出现在Models/和Backup/两个地方很常见,这时候保留带修改时间更晚的那份,另一份删掉,后面做批量处理时就不用纠结处理的是哪个副本。

3. 打开十几年前的 UML 模型:低版本兼容、XMI 中转与文本化三条路

三条路的取舍很清楚:只是想看看图长什么样,走第一条;要把图改成自己项目的,走第二条;要把模型纳入版本管理长期维护,走第三条。别一上来就奔着第三条去,转文本是有信息损失的,图形布局基本保不住,先确认原图长什么样再动手。

3.1 路径一:只读副本 + 低版本兼容打开

第一个动作永远是复制一份,把副本设成只读。老建模工具有个通病:打开工程时不提示就按自己理解的格式重写一遍,保存之后原文件就回不去了。只读属性至少在保存时会弹个窗,给你一次反悔的机会。

在 Windows 上,给整个目录加只读可以直接用命令行,比右键属性一个个点快得多:

# 在解压后的资源根目录执行,把模型工程整体设为只读 attrib +R "models\\*.*" /S # 需要放开时 attrib -R "models\\*.*" /S

/S表示递归子目录,+R是加只读、-R是去掉。这一步做完,用资源管理器看文件图标会带个小锁,心里就有底了。

环境层面,老工具常见的三个坎:一是 32 位运行时依赖,新系统上装完启动报缺少某个运行库;二是 Java 运行时版本,需要特定主版本号才肯启动,版本太新反而起不来;三是安装目录不能带中文和空格。第三条最容易被忽视,装在D:\建模工具\下面启动失败,换到D:\tools\modeler\就好了,这种玄学问题排查起来能耗掉一晚上。稳妥做法是把工具装进一台隔离的虚拟机,快照留一份,装崩了直接回滚。

打开之后先别动图,把每个.mdl过一遍,记下哪些能正常渲染、哪些报错。报错的单独列出来,走第二条路。

3.2 路径二:XMI 中转的导出参数与导入顺序

XMI 是 OMG 定的模型交换格式,几乎所有建模工具都支持导入导出,是跨工具唯一靠谱的桥。但导出时的勾选项决定了你导出来的是“一张图”还是“一堆没有位置的元素”。

导出选项建议值不这样设的后果
XMI 版本2.1,其次 2.4.1老工具多半只认到 2.1,选高了导入端直接拒收
图形布局(Diagram Interchange)勾选不勾只剩语义,导进去是一堆散点,得手工重排
构造型与标记值勾选«include»、«extend»这类关系会退化成普通关联
文本编码UTF-8中文用例名变成问号,改都没法改
ID 冲突处理导入时新建 GUID直接合并会覆盖同工程里的既有元素

操作顺序也有讲究,别反过来:

  1. 在能打开原工程的那台机器上,逐个工程导出 XMI,一个工程一个文件,文件名带上章节号和原工程名。
  2. 导出完立刻用文本编辑器打开其中一个,搜一下中文标签,确认不是问号或&#26041;这种实体乱码。
  3. 在目标工具里先建一个空工程,用“导入”而不是“打开”,导入时选新建 GUID。
  4. 导入后先数元素:类数量、关联数量、图数量,和原工具里显示的对照,对不上就说明有元素没导过来。
  5. 最后再打开图看布局,布局丢了就接受现实,语义对了就够用。

第 4 步的核对最容易被跳过,但它是唯一能在十分钟内判断导入是否完整的办法。数目对不上,八成是 XMI 版本选高了,或者导出时没勾构造型。

3.3 路径三:把类图抽成文本,扔进版本库

图形布局在文本化过程中必然丢失,但类名、属性、关联这些语义结构能完整保留,而且文本能 diff、能 code review、能跟着代码一起演进。要把 XMI 里的类图抽出来,靠标准库就够:

import csv import xml.etree.ElementTree as ET UML = "{http://www.omg.org/spec/UML/20131001}" XMI = "{http://www.omg.org/spec/XMI/20131001}" root = ET.parse("ch06-class.xmi").getroot() # 第一遍:把 xmi:id 映射到类名,后面解析关联要用 id2name = {} for el in root.iter(UML + "Class"): name = el.get("name") if name: id2name[el.get(XMI + "id")] = name # 第二遍:关联两端的类型,过滤掉解析不到名字的 relations = set() for assoc in root.iter(UML + "Association"): ends = [] for prop in assoc.iter(UML + "Property"): ends.append(id2name.get(prop.get("type"))) ends = [e for e in ends if e] if len(ends) == 2 and ends[0] != ends[1]: relations.add(tuple(sorted(ends))) with open("classes.csv", "w", newline="", encoding="utf-8-sig") as f: csv.writer(f).writerows([["class"]] + [[n] for n in sorted(id2name.values())]) with open("relations.csv", "w", newline="", encoding="utf-8-sig") as f: csv.writer(f).writerows([["source", "target"]] + [list(r) for r in sorted(relations)]) print(len(id2name), "classes,", len(relations), "associations")

关键在命名空间。XMI 2.x 的元素都挂在 OMG 的 UML 命名空间下,不同工具导出的命名空间字符串可能带年份差异,20131001是最常见的一版;如果root.iter()一个类都没找到,先把根节点的xmlns打出来看看实际值,改一下常量就行,别怀疑代码逻辑。

xmi:id是元素唯一标识,关联边不直接存类名,而是存两端的 id 引用,所以必须先建映射表再解析边。关联去重用了sorted加set,因为无向关联在 XMI 里两端顺序不固定,不去重会出现同一条边正反各一条。

导出的两个 CSV 直接提交进版本库,配合一段脚本生成类清单,比提交一堆二进制工程文件友好得多。类图和关联图能这样处理,用例图稍微麻烦点——参与者和用例之间靠关联连,但«include»是带构造型的依赖,抽取时要把构造型名一起带上,否则重构完就分不清谁包含谁了。

4. 把示例图改成自己的图:用例、类图、时序图的复用与重命名

光盘里的示例基本都是电商、图书管理、教务系统这类经典题目,改起来工作量不大,但改错的地方很隐蔽:图改了、文档没改,或者图里的类改了、时序图里的消息名没跟着变。复用的正确姿势是先建映射表,所有改动从一个地方出发。

4.1 用例图与类图:先列名词映射表,再批量替换

映射表就是两列 CSV:旧名称、新名称。旧名称按原图里的原文抄,一个字符都别改,包括全角括号和空格。新名称用你自己项目的术语,同一概念在全盘保持唯一写法——“订单”和“定单”混用是后面所有对齐问题的根源。

import csv, pathlib, re # rename.csv 两列:old,new,先用表格软件核对一遍再跑 mapping = dict(csv.reader(open("rename.csv", encoding="utf-8-sig"))) # 长词优先,避免“订单明细”被“订单”先替换掉一半 pattern = re.compile("|".join(re.escape(k) for k in sorted(mapping, key=len, reverse=True))) target = pathlib.Path("out/xmi") for f in target.glob("*.xmi"): text = f.read_text(encoding="utf-8", errors="replace") new = pattern.sub(lambda m: mapping[m.group(0)], text) if new != text: f.write_text(new, encoding="utf-8") print("updated", f.name)

三个细节决定这段代码靠不靠谱。sorted(..., key=len, reverse=True)是必须的,不做长词优先,“订单明细”会被替换成“采购单明细”这种半截结果,而且替换顺序还依赖字典顺序,不稳定。errors="replace"是防御性的,老文件的编码声明经常和实际字节对不上,遇到坏字节直接抛异常会中断整批处理,替换成占位符至少能跑完,跑完再回头查哪些文件有问题。只处理.xmi而不碰.mdl,是因为二进制文件做文本替换必坏。

替换完别直接信结果,回到工具里打开图,重点看三处:关联线两端有没有断、构造型有没有被误伤、注释框里的文字改没改。构造型里带尖括号的部分(比如«include»里的 include)如果在映射表里被列进去了,也会一起被换掉,这是最容易翻车的地方,改之前先把尖括号内的词从映射表里剔掉。

4.2 时序图与活动图:编号连续性和泳道标题

时序图是复用难度最高的图,因为它的信息密度全在顺序上。示例里的消息编号常写成1、1.1、1.2、2这种层级结构,你删掉一条分支,后面的编号全断,读者顺着编号找就对不上了。

改完先跑一遍连续性检查,比人眼扫强:

import re, sys text = open(sys.argv[1], encoding="utf-8").read() nums = re.findall(r"^\s*(\d+(?:\.\d+)*)\s", text, re.M) def key(n): return tuple(int(x) for x in n.split(".")) prev = None for n in nums: k = key(n) if prev is not None: same_level = len(k) == len(prev) and k[:-1] == prev[:-1] and k[-1] == prev[-1] + 1 deeper = len(k) == len(prev) + 1 and k[:-1] == prev if not (same_level or deeper): print("断号:", ".".join(map(str, prev)), "->", n) prev = k

逻辑很简单:相邻两条消息,要么是同级递增(1.1到1.2),要么是进入下一层(1到1.1),其余都算断号。re.M让^匹配每一行的开头,因为编号一般在消息行首。这个脚本只查断号不查重号,重号在时序图里更常见——复制一段分支忘了改编号,扫一眼输出里有没有重复数字即可。

活动图的坑在泳道。泳道标题是独立图元,导出图片时经常被裁到画布外,尤其是有三条以上泳道、画布宽度不够的时候。稳妥做法是导出前把画布调宽一档,或者干脆导出 SVG,让排版工具去管边界。

4.3 导出与命名:分辨率、格式、目录约定

图最终是要进 Word 或在线文档的,导出参数没设好,前面改得多漂亮都白搭。位图在文档里被放大就糊,矢量图不会,所以优先级是 SVG 高于 PNG。

用途格式关键参数
在线文档、MarkdownSVG不设分辨率,矢量无限缩放
Word、PDF(需要位图)PNG按最终显示宽度的 2 倍导出,一般 300dpi 起步
打印、答辩 PPTPNG 或 EMF300dpi,白底不透明
存档源工程 + SVG两个都留,源工程以后还要改

命名统一成章节号-图类型-序号-主题,全小写、空格换下划线。批量改名用脚本,别手改:

#!/usr/bin/env bash set -euo pipefail src="export/png" dst="docs/figures" mkdir -p "$dst" i=0 for f in "$src"/*.png; do [ -e "$f" ] || continue i=$((i+1)) base=$(basename "$f" .png) slug=$(printf '%s' "$base" | tr 'A-Z' 'a-z' | tr ' ' '_') printf -v n "%02d" "$i" cp "$f" "$dst/ch03-usecase-${n}-${slug}.png" done echo "renamed $i files"

set -euo pipefail让脚本遇到错误立刻停,不会默默产出半套文件。printf -v n "%02d"补零是为了文件名排序和文档里的图号顺序一致,序号不补零的话10会排在2前面。cp而不是mv,是为了保留原始导出文件,万一命名规则要改还能重跑。

5. 五种翻车现场:乱码、布局丢失、文件被覆盖怎么排查

5.1 打开全是方块和问号

现象:模型文件能打开,但里面的中文标签全部显示成方块,或者一串问号。

原因有两个层面。一是文件编码,老工具默认按本地编码写文件,换到 UTF-8 环境里就解不出来;二是字体缺失,光盘里自带的字体没装,工具用默认字体渲染中文就是方块。两者表现很像,区分办法是看英文标签正不正常——英文正常而中文全乱,是编码;连英文都显示成方块,是字体。

解决:先确认编码。用文本编辑器打开对应的.uml或.xmi,看文件头有没有encoding="UTF-8"声明;如果声明是 UTF-8 但内容确实是乱码,说明源文件本身就是本地编码存的,用编辑器的“重新以 GBK 打开”试一次,能看到正常中文就另存为 UTF-8。字体问题更好办,把光盘里fonts/目录下的字体装进系统,重启工具即可。

5.2 双击光盘菜单毫无反应

现象:挂载镜像后双击根目录的启动程序,没有任何反应,或者一闪而过。

原因:这套菜单是十几年前做的,依赖的播放器和运行库在新系统上根本不存在,有些还是 16 位程序,64 位系统直接不执行。杀软也可能把它当可疑行为拦下来。

解决:别跟菜单较劲,直接浏览目录。菜单的唯一作用是给你一个索引,而索引在Readme或目录名里本来就有。真要还原菜单,只能在虚拟机里装老系统,为这点收益不值得。跳过菜单之后,按第 2 章那份后缀对照表挨个目录看,效率反而更高。

5.3 导入 XMI 后图变成一堆散点

现象:XMI 导入成功,元素数量也对,但打开图一看,所有类挤在左上角,连线乱七八糟。

原因:导出时没勾图形布局,或者勾了但导入端不认这套布局扩展。布局信息存在 Diagram Interchange 扩展节点里,属于 XMI 的可选部分,各家实现差异很大。

解决:先确认导出端有没有勾上布局;勾了还是不认,就别修布局了,直接按语义重排。重排也有省力办法——先把类按包分组,同包的类放在一起,再连边,比无脑拖动快得多。核心结论是:XMI 保证语义不保证布局,把它当中转语义的桥,不要当中转版式的桥。

5.4 原文件被工具“升级”覆盖

现象:打开工程时提示“此文件由旧版本创建,是否升级”,点了是,保存之后原来的工具再也打不开。

原因:格式升级是单向的,新版工具保存出来的文件,低版本读不了。有些工具还不提示,直接按新格式写回去。

解决:这事没有后悔药,只能事前防。按 3.1 的做法设只读,或者干脆在复制出来的副本上操作,原始文件只当存档。更进一步,把 2.1 生成的manifest.csv一起留档,哪天怀疑文件被动过,重新算一遍哈希一比对就知道。养成“打开前先复制、保存前先改名”的习惯,这类问题基本不会再有。

5.5 图插进文档糊成马赛克

现象:SVG 插进 Word 显示异常,换成 PNG 之后放大又糊。

原因:Word 对 SVG 的支持视版本而定,老版本直接不认;位图糊是因为导出分辨率低于最终显示尺寸。这两个问题经常一起出现,让人以为是图本身的问题。

解决:位图按“最终显示宽度的 2 倍”导出,A4 纸正文宽度约 16cm,图占一半宽度时导出宽度不低于 1600px 就够清晰。表里那些 300dpi 的说法,落到操作上就是这个意思。优先选 SVG,实在不兼容再退回高分辨率 PNG,别用截图工具截屏——截图的分辨率永远等于你屏幕的分辨率,插到文档里必然糊。

6. 交付前的三分钟验收:一张检查表和一套留档习惯

图改完不算完,交付前得有一道固定关卡,否则返工成本比画图还高。我自己的检查表就四项,每项都能在一分钟内过完。

检查项合格标准不合格的典型表现
语义对齐图里的类名在文档正文里都能搜到图改了文档没改,读者对不上号
编号连续时序图消息编号无断号无重号删了分支没重排,出现1.1直接跳1.3
导出完整每张源工程都有对应的 SVG只有源文件没导出图,别人打不开
命名规范文件名小写、无空格、带章节号一堆“未命名-1.png”堆在目录里

四项里最容易漏的是第三项。源工程在自己的机器上好好的,发给别人打开是空白,因为对方没装那个建模工具。交付物里必须同时有源文件和导出图,缺一不可。

第四项可以交给脚本,省掉人眼扫目录的时间:

import csv, os, sys rows = list(csv.DictReader(open("manifest.csv", encoding="utf-8-sig"))) problems = [] # 命名规范:文件名里不该出现空格和井号 for r in rows: name = os.path.basename(r["rel_path"]) if " " in name or "#" in name: problems.append(f"命名不规范: {r['rel_path']}") # 交付完整:每份源工程都要有同名的导出图 for r in rows: if r["ext"] in (".xmi", ".uml", ".mdl"): stem = os.path.splitext(r["rel_path"])[0] hit = [x for x in rows if x["rel_path"].startswith(stem) and x["ext"] in (".svg", ".png")] if not hit: problems.append(f"缺导出图: {r['rel_path']}") print("\n".join(problems) if problems else "全部通过")

这段的前提是每次改动后重新生成一遍manifest.csv,否则查的是旧清单,等于没查。startswith(stem)匹配的是同名不同后缀,所以导出图的命名要和源工程保持一致,这也是 4.3 那套命名规则存在的理由——命名不统一,自动化检查就无从谈起。

还有一个进阶用法:把manifest.csv和源工程一起提交进版本库,每次交付前跑一遍哈希比对,能立刻发现“谁把原图覆盖了”。这套做法平时看不出价值,出一次事故就回本了。

血泪经验是,我曾经把一批改好的图直接从工具里“另存为”,覆盖了原始工程,结果第二天发现有一版改错了方向,想退回原图对照,硬盘上已经只剩改过的版本。从那以后我每次动模型文件之前,都强制走一遍“复制到 work 目录、原件设只读、哈希入清单”这三步,慢是慢了点,但再没为找回原图花过时间。希望帮到你。

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

RELATED READING

延伸阅读

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