DTD 元素与属性对比
本章定位 :理解 DTD 设计中"何时用子元素、何时用属性"这一核心设计决策——掌握元素与属性的语义差异、设计原则与典型误区,避免最常见的 DTD 建模错误。
定义与作用
在 DTD 中,数据可以用两种方式表示:
<!-- 方式一:属性 -->
<book title="XML入门指南" author="大翔"/>
<!-- 方式二:子元素 -->
<book>
<title>XML入门指南</title>
<author>大翔</author>
</book>
这两种方式都合法,但语义截然不同。 元素与属性的选择 是 DTD 设计中最常见的争议点,也是面试中的高频问题。
核心问题:什么时候应该把一段数据建模为子元素?什么时候应该建模为属性?
核心原理:语义差异模型
图解释 :数据建模决策树揭示了核心原则——能扩展、可重复、内容长、可能含子结构的用子元素;元数据性质的标识、修饰性简短数据用属性。
语法/结构要点
元素与属性的全方位对比
| 维度 | 子元素 | 属性 |
|---|---|---|
| 语义 | 数据本身("是什么") | 数据的修饰/元信息("什么样") |
| 可扩展性 | 可以扩展,可添加子元素 | 不能扩展,属性值是纯文本 |
| 可重复性 | 可以多次出现 | 同一元素上属性名唯一 |
| 多值支持 | 自然地支持(多个子元素) | 不支持,只能一个值 |
| 特殊字符 | 可通过 CDATA 包含 <、& | 必须转义 <→< &→& |
| 空白处理 | 保留空白 | 空白被规范化 |
| 结构深度 | 可嵌套多层 | 平面结构,只能一层 |
| 顺序保证 | 有明确的子元素顺序 | 属性无序,排列任意 |
| 可编程访问 | 通过 DOM childNodes 遍历 | 通过 DOM getAttribute() 直接获取 |
| DTD 声明 | <!ELEMENT> | <!ATTLIST> |
典型设计原则
| 原则 | 说明 | 适用侧 |
|---|---|---|
| 核心数据用元素 | 数据本身、需要展示、需要搜索的内容 | 子元素 |
| 元数据用属性 | 标识(ID)、引用(IDREF)、格式化提示 | 属性 |
| 可扩展用元素 | 未来可能需要添加子结构的 | 子元素 |
| 简短约束用属性 | DTD 中属性声明能定义枚举、默认值 | 属性 |
| 人类可读的关键信息用元素 | 用户会直接看到的文本 | 子元素 |
| 机器使用的标识用属性 | ID、类型码、常量 | 属性 |
完整示例
场景说明
飞翔科技的 大翔 正在设计一个"员工信息"XML 格式。白歌审查 DTD 时,指出他把所有数据都塞成了属性——这是一种常见的设计错误。
操作前:过度使用属性的错误设计
employee_bad.xml :
<?xml version="1.0"?>
<!DOCTYPE employees [
<!ELEMENT employees (employee*)>
<!ELEMENT employee EMPTY>
<!-- 所有数据全成属性 —— 大翔的错误设计 -->
<!ATTLIST employee
id ID #REQUIRED
name CDATA #REQUIRED
age CDATA #REQUIRED
email CDATA #REQUIRED
bio CDATA #IMPLIED
skill1 CDATA #IMPLIED
skill2 CDATA #IMPLIED
skill3 CDATA #IMPLIED
>
]>
<employees>
<employee id="E001" name="大翔" age="28"
email="daxiang@feixiang.com"
bio="全栈工程师,擅长 Java 和 Python,5年经验"
skill1="Java" skill2="Python" skill3="DevOps"/>
<employee id="E002" name="白歌" age="31"
email="baige@feixiang.com"
bio="架构师,主导公司微服务转型"
skill1="Spring" skill2="K8s" skill3=""/>
</employees>
问题分析 :
bio很长,作为属性难以阅读和编辑skill用 skill1/skill2/skill3 硬编码——员工可能只有 1 个技能或 5 个技能- 属性值不能包含
<或&,bio 中的特殊字符必须转义 - 无法为技能或 bio 添加子结构(如 skill 的熟练度 level)
应用正确设计原则后
employee_good.xml :
<?xml version="1.0"?>
<!DOCTYPE employees [
<!-- 员工列表 -->
<!ELEMENT employees (employee*)>
<!-- 员工:核心数据用子元素 -->
<!ELEMENT employee (name, age, email, bio?, skills?)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT age (#PCDATA)>
<!ELEMENT email (#PCDATA)>
<!ELEMENT bio (#PCDATA)>
<!-- 技能:自然地支持多个且可扩展 -->
<!ELEMENT skills (skill+)>
<!ELEMENT skill (#PCDATA)>
<!-- 属性:仅用于标识和元信息 -->
<!ATTLIST employee
id ID #REQUIRED <!-- 唯一标识:属性 -->
dept (研发部|产品部|测试部) #REQUIRED <!-- 枚举约束:属性 -->
level CDATA #IMPLIED <!-- 简短元信息:属性 -->
>
<!-- 技能的熟练度可以在未来扩展为子元素或属性 -->
<!ATTLIST skill
proficiency (初级|中级|高级|专家) "中级"
>
]>
<employees>
<employee id="E001" dept="研发部" level="T3">
<name>大翔</name>
<age>28</age>
<email>daxiang@feixiang.com</email>
<bio>全栈工程师,擅长 Java 和 Python,5年经验。
专注方向:后端架构 & DevOps(转义后安全输出)</bio>
<skills>
<skill proficiency="高级">Java</skill>
<skill proficiency="高级">Python</skill>
<skill proficiency="中级">DevOps</skill>
</skills>
</employee>
<employee id="E002" dept="研发部" level="T5">
<name>白歌</name>
<age>31</age>
<email>baige@feixiang.com</email>
<bio>架构师,主导公司微服务转型,深度参与 K8s 集群建设</bio>
<skills>
<skill proficiency="专家">Spring</skill>
<skill proficiency="高级">K8s</skill>
</skills>
</employee>
</employees>
操作结果
| 对比维度 | 错误设计 | 正确设计 |
|---|---|---|
| 阅读体验 | bio 挤在一行属性中,难读 | 结构清晰,分块明确 |
| 技能数量 | 硬编码 3 个属性位 | 灵活,有多少写多少 |
| 技能扩展 | 无法添加熟练度 | 已添加 proficiency 属性 |
| 特殊字符 | 属性值不能含原始 < & | 子元素内容可含转义后的字符 |
| 搜索便利 | 属性值无法用 XPath 匹配文本 | 子元素天然支持 XPath 文本匹配 |
易错场景
属性全塞
把标题、正文、描述等所有数据都定义为属性——这是 XML 初学者最常见的设计错误。
<!-- 灾难性设计 — 不要这么做 -->
<book title="XML" author="大翔" chapter1="概述" chapter2="语法" ... price="59"/>
当一段数据可能变长、需要换行、可能包含特殊字符时,它就不适合成为属性。
用多个同名属性模拟多值
<!-- 错误:属性不允许重复 -->
<employee skill="Java" skill="Python" skill="DevOps"/>
<!-- XML 解析器报错:duplicate attribute "skill" -->
DTD/XML 规范中同一元素上属性名不能重复。多值必须用子元素。
ID/IDREF 绑定用错数据类型
<!ATTLIST order
order_id CDATA #REQUIRED <!-- 应该用 ID -->
customer_ref CDATA #REQUIRED <!-- 应该用 IDREF -->
>
当数据承担"唯一标识"和"跨元素引用"功能时,应该使用 ID/IDREF 类型以获得 DTD 验证器的约束检查。
面试考点
| 考点 | 参考答案要点 |
|---|---|
| 何时用子元素、何时用属性? | 核心数据、可重复、可扩展、含结构的用子元素;标识、简短元信息、枚举约束用属性 |
| 属性相比子元素的 3 个关键限制? | ①不能重复(属性名唯一);②不能嵌套(平面结构);③不能直接包含 < &(必须转义) |
| 为什么 DTD 设计不鼓励过度使用属性? | 难以扩展、阅读体验差、多值需要硬编码多个属性、无法满足复杂数据结构需求 |
| 属性有什么子元素没有的优势? | DTD 中可声明枚举、默认值、ID/IDREF 类型校验;访问时 getAttribute() 比 childNodes 遍历快 |