
本文介绍如何通过多态设计模式将数据对象的渲染职责(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;
- 关注点分离清晰:
- 可测试性强:每个 Renderer.render() 方法可独立单元测试,输入数据 → 输出 HTML 字符串,无副作用;
- 支持渐进重构:可先从高频字段(如 image, date)开始抽取 Renderer,再逐步迁移其余逻辑;
- 注意 xss 防护:示例中 escapeHtml() 必须实现(推荐使用 DOMPurify.sanitize() 或原生 textContent 替代内联插值);
- 避免过度设计:若仅有 2–3 种简单字段,可先用工厂函数 + 纯函数渲染器(renderText(row)),再演进为类。
? 进阶提示:结合现代框架(React/Vue)时,该模式自然映射为「组件化」——SaleRecordRenderer 即 SaleRecordCell.vue;而在纯 DOM 场景中,它正是你缺失的、面向对象的 UI 分层基石。
通过将“绘制自己”的能力从模型中剥离并交由多态 Renderer 承担,你不仅解决了代码臃肿问题,更构建了一套可生长、易协作、抗变化的前端架构范式。