面向对象渲染:用多态替代条件分支实现展示逻辑解耦

5次阅读

面向对象渲染:用多态替代条件分支实现展示逻辑解耦

本文介绍如何通过多态设计模式将数据对象的渲染职责(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 承担,你不仅解决了代码臃肿问题,更构建了一套可生长、易协作、抗变化的前端架构范式。

text=ZqhQzanResources