
本文介绍如何通过多态设计模式将数据对象的渲染职责(html生成、ui控件绘制)与业务逻辑(数据验证、状态管理、交互处理)彻底分离,避免单个类膨胀至千行代码,提升可维护性与可扩展性。
本文介绍如何通过多态设计模式将数据对象的渲染职责(html生成、ui控件绘制)与业务逻辑(数据验证、状态管理、交互处理)彻底分离,避免单个类膨胀至千行代码,提升可维护性与可扩展性。
在 Web 应用中,当“数据模型”(如 Row 或 Collection)同时承担数据管理、状态变更、用户交互和 UI 渲染多重职责时,极易陷入“上帝对象”困境——例如一个 Row 类因需支持 10+ 种字段类型(文本、复选框、图片上传、日期选择器等)而突破 1000 行,导致修改风险高、测试困难、复用性差。
根本解法不是简单地按文件拆分(如 row.js + row_display.js),也不是另建一个通用构造器(如 HtmlTableConstructor)来集中处理所有渲染逻辑——后者会演变为新的巨型过程式模块,违背单一职责原则。真正可持续的架构升级,是将“如何渲染自己”这一能力,下放给每个具体数据类型的专属渲染器,通过多态实现动态委派。
✅ 推荐方案:策略模式 + 多态渲染器(Renderer Pattern)
核心思想:让每种数据类型拥有自己的渲染器(Renderer),模型对象只负责持有数据与行为,渲染动作委托给对应的 Renderer 实例。
示例结构(ES6 Class + Composition)
// models/Row.js
class Row {
constructor(data, schema) {
this.data = data;
this.schema = schema; // e.g., { field: 'status', type: 'radio', options: [...] }
}
// 渲染委托:不写 HTML,只决定用哪个 renderer
getRenderer() {
const type = this.schema.type;
switch (type) {
case 'text': return new TextRenderer(this);
case 'checkbox': return new CheckboxRenderer(this);
case 'image': return new ImageRenderer(this);
case 'sale-record': return new SaleRecordRenderer(this); // 业务专属
default: throw new Error(`No renderer for type: ${type}`);
}
}
}
// renderers/TextRenderer.js
class TextRenderer {
constructor(row) {
this.row = row;
}
render() {
return `<td><input type="text" value="${escapeHtml(this.row.data.value)}"></td>`;
}
}
// renderers/SaleRecordRenderer.js —— 可独立开发、测试、复用
class SaleRecordRenderer {
constructor(row) {
this.row = row;
}
render() {
const { amount, currency, date } = this.row.data;
return `
<td class="sale-cell">
<strong>${amount} ${currency}</strong>
<div class="date">${new Date(date).toLocaleDateString()}</div>
<button onclick="editSale(${this.row.data.id})">编辑</button>
</td>
`;
}
// 也可封装交互逻辑(与视图强相关部分)
bindEvents() {
document.querySelector(`[data-sale-id="${this.row.data.id}"] .edit-btn`)
.addEventListener('click', () => this.handleEdit());
}
handleEdit() { /* 业务专属编辑流程 */ }
}在 Collection 中统一协调
// models/Collection.js
class Collection {
constructor(rows, schema) {
this.rows = rows;
this.schema = schema;
}
renderTable() {
const headers = this.schema.map(f => `<th>${f.label}</th>`).join('');
const bodyRows = this.rows.map(row => {
const renderer = row.getRenderer();
return renderer.render(); // 多态调用,无需 if/else
}).join('');
return `
<table class="data-table">
<thead><tr>${headers}</tr></thead>
<tbody>${bodyRows}</tbody>
</table>
`;
}
// 批量挂载交互事件(可选)
mountInteractions() {
this.rows.forEach(row => {
const renderer = row.getRenderer();
if (typeof renderer.bindEvents === 'function') {
renderer.bindEvents();
}
});
}
}? 关键优势与实践建议
- 开闭原则落地:新增字段类型(如 signature-canvas)只需新增 SignatureRenderer 类,无需修改 Row 或 Collection;
-
关注点分离清晰:
- Row:专注数据结构、校验规则、CRUD 接口;
- Renderer:专注视觉呈现、DOM 操作、轻量级事件绑定;
- 可测试性强:每个 Renderer.render() 方法可独立单元测试,输入数据 → 输出 HTML 字符串,无副作用;
- 支持渐进重构:可先从高频字段(如 image, date)开始抽取 Renderer,再逐步迁移其余逻辑;
- 注意 XSS 防护:示例中 escapeHtml() 必须实现(推荐使用 DOMPurify.sanitize() 或原生 textContent 替代内联插值);
- 避免过度设计:若仅有 2–3 种简单字段,可先用工厂函数 + 纯函数渲染器(renderText(row)),再演进为类。
? 进阶提示:结合现代框架(React/Vue)时,该模式自然映射为「组件化」——SaleRecordRenderer 即 SaleRecordCell.vue;而在纯 DOM 场景中,它正是你缺失的、面向对象的 UI 分层基石。
通过将“绘制自己”的能力从模型中剥离并交由多态 Renderer 承担,你不仅解决了代码臃肿问题,更构建了一套可生长、易协作、抗变化的前端架构范式。










