14.5 转换视图
把领域数据交给显式转换规则,生成 HTML、PDF 或邮件等输出,让呈现逻辑集中在可测试的转换边界。
学习目标
- 能沿“HTTP 请求 → 呈现模型 → 转换规则 → 输出格式”标出数据与责任边界,并指出业务决定不应藏在转换器里
- 能编写一个纯函数转换器,把订单呈现模型稳定地变成安全 HTML,并为同一输入编写可重复的断言
- 能根据输出格式数量、设计协作和失败定位证据,选择转换视图,或说明何时应改用模板视图与两步视图
为什么 14.5 转换视图 值得单独学习
订单后台常常既要返回 HTML 页面,又要导出 PDF 或发送邮件。若每种输出都重新查询领域对象、判断业务状态并拼接页面,变化会沿着请求路径复制,任何一个小修复都可能只改到其中一种格式。转换视图把问题收窄成一条可验证的链:应用先准备稳定的呈现数据,再由显式转换规则生成一种输出。
这里的关键不是记住 XSLT 或某个模板库的 API,而是能回答三个问题:谁拥有业务决定,输入是否已经是可呈现的数据,转换失败能否在响应发送前被定位。转换视图适合把“结构化数据如何成为某种输出”做成独立、可测试的代码;它不授权视图层反过来修改订单或访问数据库。
先建立直觉:同一订单如何得到不同输出
先预测:订单 id=42、总额 597、包含两个明细时,HTML 与邮件正文应该共享哪一份输入?如果 HTML 输出中的标题改了,哪一条规则应该变化,哪一条业务不变量必须保持不动?答案是让输出差异停留在转换边界,而不是让每种输出重新拥有一套领域流程。
把流程固定为“HTTP 请求 → 控制器 → 应用模型 → 呈现模型 → 转换器 → HTTP 响应”。控制器解释请求并调用用例,应用服务决定订单能否被查看;转换器只读取呈现模型,按规则生成目标格式。这样,失败也有清晰的停止点:缺少订单时在应用服务结束,非法字符在编码/转义边界被处理,规则缺陷则在转换测试中暴露。
什么是转换视图
↡把已准备好的呈现数据按显式转换规则变成 HTML、PDF 或邮件等目标输出的视图实现不是“把所有渲染代码搬到另一个文件”,而是一条数据到输出的窄边界。它可以使用 XSLT、专门的转换函数或其他声明式规则;具体技术可以替换,但输入必须保持可检查,转换不能偷偷读取请求上下文或修改领域状态。
转换视图首先接收 ↡为某个输出准备、只包含呈现所需字段的稳定数据形状。呈现模型不是数据库实体的别名:它可以把订单总额格式化为展示所需的数值,把权限允许的字段显式列出,并把缺失数据表示为可处理的状态。将领域对象直接交给转换器,会让输出规则重新猜测业务含义,测试也会被持久化结构绑住。
随后,↡把呈现模型中的字段、集合与条件映射到目标文档结构的可审计规则读取模型并生成输出。规则可以遍历订单明细、选择标题和拼接链接,但不应该决定“订单是否可以取消”或打开数据库。把这种决定留在应用服务,能让 HTML、PDF 和邮件共享同一条业务判断。
输出最终落在一个 ↡转换器承诺生成的具体结果形态,例如 HTML、PDF 或纯文本邮件上。格式不同并不代表业务流程不同:同一个呈现模型可以送给不同转换器。为了不让“可复用”变成口号,需要记录每个格式的转义边界、缺失字段处理和失败响应,并用同一输入重放这些规则。
目录单元到教学证据
14.5 转换视图
本章只覆盖目录单元 14.5 转换视图,单元键为 poeaa24-pattern-29-transform-view,模式族为 web。评审证据固定为一笔订单:id=42、总额 597、两个明细和一个客户显示名。学习者要能从这笔输入追踪呈现模型、转换规则和输出格式,并指出故障在何处停止。
本章的裁决是:如果多个输出共享同一份已准备数据,且格式变化可以隔离在转换规则中,就可以考虑转换视图;如果设计人员需要直接编辑接近最终 HTML 的文档,或第二步全局布局才是主要变化,应比较模板视图和两步视图。工具、框架和部署平台变化后,这张证据卡仍应能复核同一条责任链。
专属代码案例:订单后台的纯转换边界
下面的 TypeScript 草图把业务判断和呈现规则分开。buildOrderViewModel 接收应用服务已经确认过的订单,toHtml 只把呈现模型转成 HTML;它不查询数据库,也不改变输入对象。真实项目可以把 toPdf 或 toEmailText 接到同一个呈现模型上。
type OrderViewModel = {
id: string;
customerName: string;
total: number;
items: Array<{ name: string; quantity: number }>;
};
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
export function toHtml(model: OrderViewModel): string {
const items = model.items
.map((item) => `<li>${escapeHtml(item.name)} × ${item.quantity}</li>`)
.join("");
return [
`<h1>订单 ${escapeHtml(model.id)}</h1>`,
`<p>客户:${escapeHtml(model.customerName)}</p>`,
`<p>合计:¥${model.total.toFixed(2)}</p>`,
`<ul>${items}</ul>`,
].join("\n");
}这段代码有两个可测试的不变量:相同的 OrderViewModel 产生相同的 HTML,名称中的 < 不会作为标签执行。它没有展示“订单是否已支付”的判断,因为那属于应用服务;如果转换规则为了补齐一个字段而重新查询订单表,评审就应拒绝这条边界。另一个测试可以把 toHtml 和 toEmailText 作用于同一呈现模型,确认输出格式变化没有复制业务决策。
三步交互实验:输入、转换与拒绝边界
按顺序观察专属图。每一步都写下“当前输入—责任所有者—可推翻证据”:如果图中只看见最终 HTML,却找不到呈现模型或转换规则,就不能把一次成功渲染当成模式证据。
第 1 / 4 步 · 应用服务先准备稳定的呈现模型
按步骤核对输入、转换和输出的责任;最后注入越界故障,再重置并重放。
1. 固定呈现模型,隔离请求解释
控制器和应用服务先把订单整理成呈现模型。转换视图只接收 id、客户显示名、总额和明细,不重新读取原始请求,也不直接接触数据库。
选择与拒绝矩阵
| 评审问题 | 选择转换视图的证据 | 应拒绝或改用其他方案的信号 |
|---|---|---|
| 输入 | 转换器接收稳定的呈现模型,字段来源可以追溯 | 转换器自己读取请求、权限上下文或数据库 |
| 变化 | HTML、PDF 和邮件的差异集中在各自规则中 | 一个格式的小改动复制到控制器和多个业务服务 |
| 安全 | 文本字段在明确的编码/转义边界处理 | 规则把用户输入拼成标签,却没有可重放测试 |
| 协作 | 规则可独立测试,输出契约由代码审查 | 设计人员需要直接编辑接近最终 HTML 的模板 |
| 替代 | 已比较模板视图与两步视图,并保留迁移路径 | 只是因为框架提供转换 API 就跳过责任分析 |
转换视图与模板视图都能生成 HTML,但关注点不同:转换视图强调“结构化输入如何经过规则得到输出”,模板视图更接近最终文档结构,通常更便于页面设计协作。两步视图则把页面内容和全局布局拆成两个阶段;如果导航、页脚和换肤才是主要变化,应认真比较它,而不是把布局继续塞进每个转换规则。
常见误区
本章小结
- 转换视图把稳定的呈现模型交给显式转换规则,再生成目标输出。
- 业务判断归应用服务,转换器不查库、不改状态,只负责可测试的表示转换。
- 同一呈现模型可以服务 HTML、PDF 或邮件,但每种格式都要声明转义、缺失字段和失败契约。
- 设计协作偏向文档模板,或全局布局变化占主导时,应比较模板视图和两步视图。
- 一旦图中出现“转换器直接访问数据库或改变订单”,应拒绝该实现并把责任移回正确边界。
本章练习
练习
问题 1:边界推理。 toHtml 收到一个客户名 <script>alert(1)</script>。应在哪一层处理它?为什么不能让模板或数据库替转换器决定?
问题 2:改 Demo 代码。 现在还要输出纯文本邮件。怎样复用订单数据,又怎样证明邮件格式没有复制“订单是否可取消”的业务判断?
问题 3:场景选型。 设计人员需要频繁调整导航、页脚和页面结构,而不同输出也要复用订单内容。你会保留转换视图还是迁移?请写出一个可观测的迁移信号。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 转换视图
接收已准备的呈现数据,按显式规则生成一种或多种目标输出的视图实现。
- 呈现模型
为输出准备、只携带渲染所需字段的稳定数据形状,不等同于数据库实体。
- 转换规则
把呈现模型的字段、集合和条件映射到目标文档结构的可审计规则。
- 输出格式
转换器承诺生成的结果形态,例如 HTML、PDF 或纯文本邮件。
- 转义边界
用户可控文本从数据进入文档语法前,必须被编码或转义的明确位置。