用实体继承与注册表统一预算卡片
场景
租金、押金和采购预算单共用同一种卡片布局,都要展示预算单号、申请人和状态,但各自的业务字段和可用操作不同。把这些差异收进对应实体,卡片就可以按统一结构渲染。
基类提供公共字段和操作,子类通过 super 复用后追加自己的内容。每个操作包含 key、label 和 execute:例如租金实体既定义「上传合同」按钮,也提供点击后调用的函数。
渲染时,工厂根据 budget.type 创建实体,UI 读取字段和操作。此时 execute 只是一个函数,不会执行;点击按钮后才调用它,触发对应业务函数。
代码
这里只保留预算类型到实体类的注册表。操作直接写在 getActions() 返回的对象里,卡片统一绑定 onClick={() => action.execute()}。
实体直接导入业务函数。配套的 budget-actions.js 为独立阅读提供了输出调用参数的示例函数,不会创建真实弹窗;接入项目时替换为实际弹窗实现。按钮设置 type="button",避免卡片放进表单时意外提交。
基类提供公共字段与操作
import {
openDetailModal,
openEditModal,
openRefundModal,
openUploadModal
} from './budget-actions.js'
// 1. 基础实体:公共字段和操作
class BaseBudgetEntity {
constructor(data) {
this.data = data
}
getFields() {
return [
{
key: 'id',
label: '预算单号',
value: this.data.id
},
{
key: 'applicant',
label: '申请人',
value: this.data.applicant
},
{
key: 'status',
label: '状态',
value: this.data.status
}
]
}
getActions() {
return [
{
key: 'detail',
label: '查看详情',
execute: () => openDetailModal(this.data)
}
]
}
}子类追加字段,并直接提供操作函数
// 2. 业务实体:只维护各自的差异
class RentBudgetEntity extends BaseBudgetEntity {
getFields() {
return [
...super.getFields(),
{
key: 'rentAmount',
label: '租金',
value: this.data.rentAmount
},
{
key: 'rentPeriod',
label: '租期',
value: this.data.rentPeriod
}
]
}
getActions() {
return [
...super.getActions(),
{
key: 'edit',
label: '编辑',
execute: () => openEditModal(this.data)
},
{
key: 'uploadContract',
label: '上传合同',
execute: () => openUploadModal(this.data)
}
]
}
}
class DepositBudgetEntity extends BaseBudgetEntity {
getFields() {
return [
...super.getFields(),
{
key: 'depositAmount',
label: '押金',
value: this.data.depositAmount
},
{
key: 'refundDate',
label: '退还日期',
value: this.data.refundDate
}
]
}
getActions() {
return [
...super.getActions(),
{
key: 'refund',
label: '退款',
execute: () => openRefundModal(this.data)
}
]
}
}
class PurchaseBudgetEntity extends BaseBudgetEntity {
getFields() {
return [
...super.getFields(),
{
key: 'amount',
label: '采购金额',
value: this.data.amount
},
{
key: 'supplier',
label: '供应商',
value: this.data.supplierName
}
]
}
getActions() {
return [
...super.getActions(),
{
key: 'edit',
label: '编辑',
execute: () => openEditModal(this.data)
}
]
}
}按预算类型创建实体
// 3. 注册表:预算类型 → 实体类
const budgetEntityRegistry = new Map([
['rent', RentBudgetEntity],
['deposit', DepositBudgetEntity],
['purchase', PurchaseBudgetEntity]
])
// 4. 工厂:根据类型创建实体,未知类型退回公共展示
function createBudgetEntity(data) {
const EntityClass =
budgetEntityRegistry.get(data.type) ?? BaseBudgetEntity
return new EntityClass(data)
}卡片统一渲染和绑定点击
// 5. UI:统一渲染,点击时调用操作自带的 execute
function BudgetCard({ budget }) {
const entity = createBudgetEntity(budget)
const fields = entity.getFields()
const actions = entity.getActions()
return (
<div>
{/* 字段展示 */}
{fields.map(field => (
<div key={field.key}>
<span>{field.label}</span>
<span>{field.value}</span>
</div>
))}
{/* 行为展示 */}
{actions.map(action => (
<button
type="button"
key={action.key}
onClick={() => action.execute()}
>
{action.label}
</button>
))}
</div>
)
}记一下
三种类型分别得到什么
所有类型都会先展示预算单号、申请人和状态,并提供「查看详情」。下面是各自追加的部分:
| 类型 | 追加字段 | 追加操作 |
|---|---|---|
rent | 租金、租期 | 编辑、上传合同 |
deposit | 押金、退还日期 | 退款 |
purchase | 采购金额、供应商 | 编辑 |
| 未注册类型 | 无 | 无 |
例如租金预算会得到 5 个字段和 3 个操作。创建实体、读取操作或渲染卡片都不会打开弹窗,点击「上传合同」才调用 openUploadModal(this.data)。
为什么用箭头函数
execute: () => openUploadModal(this.data) 把调用延后到点击时,并捕获当前实体的 this。不要写成 execute: openUploadModal(this.data),那会在读取操作列表时立即执行。
super.getActions() 复用基类的「查看详情」,子类再追加自己的操作;super.getFields() 同样复用公共字段。
新增内容时改哪里
| 变化 | 修改位置 |
|---|---|
| 新增预算类型 | 新增实体类,并加入 budgetEntityRegistry |
| 某种预算新增操作 | 在对应实体的 getActions() 中追加 { key, label, execute };全新能力还需实现业务函数 |
| 改变某个操作的具体行为 | 对应业务函数,必要时调整实体中的 execute |
| 改变字段或按钮的布局 | BudgetCard |
只要字段和操作保持相同结构,新增类型或操作就不需要去卡片里补分支。如果新增的是特殊控件或全新布局,UI 仍然需要扩展。
这个简化版本的取舍
实体决定展示哪些字段、提供哪些操作以及操作的执行入口;业务函数实现弹窗等交互;UI 遍历字段和按钮,在点击时调用 execute()。
实体因此直接依赖业务函数。对于只服务于预算页面的代码,这样容易定位和修改。如果以后同一个实体要用于不同页面、替换交互实现,再考虑把业务能力从外部传入。
未知类型会退回基类,只显示公共信息和「查看详情」。这是示例的兜底策略,需要严格识别类型时可以改成报错。示例没有实现状态判断、权限校验或金额格式化。
配置表是什么
配置表就是一个普通 JavaScript 对象,按类型保存字段和操作。例如:
const budgetConfig = {
rent: {
fields: [
{ key: 'rentAmount', label: '租金' },
{ key: 'rentPeriod', label: '租期' },
],
},
deposit: {
fields: [
{ key: 'depositAmount', label: '押金' },
{ key: 'refundDate', label: '退还日期' },
],
},
}公共函数通过 budgetConfig[budget.type] 取出配置,拼接公共字段,再从 budget 取值即可。配置里也可以放操作函数。
如果差异主要是固定字段和按钮名称,配置表通常更直接;如果各类型逐渐出现独立的计算和判断,实体类可以集中承载这些逻辑。操作带上 execute 并不意味着必须使用类,选择应以是否容易理解和维护为准。
面试时怎么讲
这段设计把公共字段和操作放进基类,各业务子类追加自己的差异,工厂根据预算类型创建对应实体。操作同时带有名称和执行函数,UI 只负责统一渲染并绑定点击,因此新增某种预算的操作可以集中在对应实体中修改。为了保持简单,实体直接调用业务函数,代价是与具体交互实现存在耦合;如果差异只是固定配置,也可以用普通对象实现。