

昨天结尾说今天要谈框架该替用户决定哪些事。
框架与函数库的差别常被说成「你调用函数库,框架调用你」。这句话描述的是机制,不是代价。真正的代价是控制权反转之后,框架的默认值就变成你的默认值:它决定数据长什么样子、请求怎么流动、扩展点开在哪里。你可以不同意,代价是绕过它,而绕过一套框架通常比不用它更贵。
所以评价一套框架不该问「它能做什么」。能做的事越多,代表它替你决定的事越少,同样的决定,每一张表单都得再做一次。
本篇回答 Definition-Driven 框架必须先定案的四个问题,其余设计全部是这四个答案的推论:
- 系统的唯一真相放在哪一层
- 哪些层由它衍生
- 哪些留给应用自己做
- 这一整套在什么样的系统上不成立
这四题没有一题可以留白。框架不回答,用它的人就会各自回答,而且答案彼此不同:这个模块把真相放在 Entity,那个模块放在数据表,第三个放在前端的表单配置文件。昨天收敛的三个老问题,很大一部分就是这样长出来的。
一、唯一真相放在哪一层
先澄清一件容易混淆的事:Code-First 的堆栈里并不缺定义。API 契约是一份定义,Migration 脚本是一份定义,数据库的元数据也是。差别不在有没有定义,而在方向:那些定义是被倒出来的产物,改它们没有意义,下一次产生就会被盖掉。
所以问题不是「要不要有定义文件」,是箭头该往哪边指。候选只有三个:
| 候选 | 代表范式 | 问题 |
|---|---|---|
| 代码 | Code-First | 类型系统够宽,什么语义都塞得进去。代价是真相的读者只有编译器,改真相就要走编译、打包、部署 |
| 数据库 | Database-First | 数据是最终事实,但类型系统太窄 |
| 定义 | Definition-Driven | 需同时满足两个条件 |
数据库窄在哪里:它知道 total_amount 是 decimal(18,4),但不知道这个数字是金额、数量还是汇率,不知道该不该四舍五入、显示几位、必不必填、能不能修改、要不要出现在查询清单上。这些语义它承载不了,最后还是得有另一个地方补上,真相又散开了。
定义要当真相,两个条件缺一不可
- 够宽,宽到能承载结构以外的语义
- 它是数据,不是代码(昨天整篇的主题)
这套框架里一个字段能声明的东西,可分成六类:
| 类 | 声明的内容 | 数据库放得下吗 |
|---|---|---|
| 结构 | 字段名、数据类型、长度、默认值 | |
| 行为 | 必填、只读、是否显示 | 部分 |
| 呈现 | 标题、控件类型、显示格式、宽度 | |
| 语义 | 数值种类、币种字段、计量单位字段 | |
| 关联 | 关联来源表单、字段对应、显示字段 | |
| 安全 | 数据范围角色、敏感数据分类 |
代码六类都放得下,但要靠一叠 attribute 堆出来,堆完还是得有人把它们解析成数据才用得上。
这份声明的宽度是刻意的:要一次盖住 UI、数据库与验证三个面向。
唯一真相不是宣称出来的,是依赖方向决定的
任何框架都可以在文档里写「本框架以某某为唯一真相」。要验证,看依赖方向就够了:真相那一层应该谁都不依赖,而所有人依赖它。
定义层这个包的引用清单极短,它不知道有数据库、不知道有 HTTP、不知道有 UI。反过来,数据访问、业务逻辑、API 契约、客户端、每一个前端,全部引用它。
这条依赖方向带来两个好处:
- 定义层可以单独被工具处理。定义编辑器、生成器、批量校验脚本,只需引用这一个包,不必把整个数据库与 API 堆栈拖进来。昨天说「上千份定义可以写一支脚本扫过去」,前提就在这里。
- AI 也是受益者。要让它理解一套系统,喂定义比让它扫整个代码库有效率,结论也更可靠。差别不在文件大小,在于关系是声明出来的还是推论出来的:问「这张订单牵连到哪些表单」,读定义是把已经声明好的关联读出来;读代码要追类型、追查询、追配置,再推论意图,而推论会出错。
省下来的 token 是附带结果。至于定义管不到的业务判断,该读代码的还是得读。
二、哪些层由定义衍生
昨天用的是白话:UI、数据库、CRUD SQL、验证规则。精确一点的清单分两种,多数是产生:
| 产生什么 | 依据 |
|---|---|
| 单笔表单布局 | 表单结构(主表区块 + 明细区块) |
| 清单布局 | 表单结构声明的清单字段 |
| 开窗选取布局 | 表单结构声明的可查询字段 |
| 数据表结构 | 表单结构的每一张表 |
| INSERT / UPDATE / DELETE | 表单结构的字段集合 |
| SELECT,含跨表 JOIN | 表单结构的字段与关联声明 |
| 跨层传输的数据容器结构 | 表单结构的字段集合 |
另一种是求值,框架不造东西,而是拿定义里声明的规则与表达式去算:验证规则在保存与删除的时点求值,计算列在用户改动相关字段的当下重算。
需求收敛出新的共通行为,就会再长出对应的机制。像报表的查询条件界面,本质上也是一组字段加上编辑器,用布局定义描述得出来。
按「谁在用它」收拢:
表单结构定义
│
├─ 产生
│ ├─ 布局三种:单笔表单 / 清单 / 开窗选取 → 各端界面
│ ├─ 数据表结构 → 数据库
│ ├─ SQL 两种:增修删 / 查询(含跨表 JOIN) → 数据访问
│ └─ 跨层数据容器结构 → 跨层传输
│
└─ 求值
├─ 验证规则 → 保存与删除的时点
└─ 计算列 → 用户改动相关字段的当下
产生的是代码,还是对象
同样从定义长出东西,发生在构建期与运行时,结果完全不同:
- 构建期生成器:产出代码。定义改了要重新产生、重新编译、重新部署,昨天那条交付路径一步都没少,前面省下的好处在最后一关全部还回去。
- 运行时衍生:产出内存里的对象。定义换了,下次读就是新的。「改定义到生效没有编译打包部署」成立的位置就在这里。
换来的代价有两个:
- 编译期类型检查没了,字段名打错要到运行时才知道(Day 9 会谈这笔账怎么补)
- 缓存失效从优化变成必解题。定义为了性能一定会被缓存,框架就得有一套通知机制,让某个节点改掉的定义传到其他每一个节点。
三、哪些留给应用
框架该停在哪里,有两条规则,一正一反。
正面:凡是制式化、标准化的行为,都该收敛成框架的机制。
这不只是少写几行代码:
- 同一件事只有一套做法,要修就修那一处(省维护成本)
- 新人、顾问、接手的人只要学一次,看懂一张表单就看得懂全部(省教育成本,这一项最容易被低估)
昨天列的那些管不动的症状,全是「没有通用机制」开出来的账单,而且是按人头、按年重复开的。
反面是一句判别法:这件事,全世界的 ERP 做起来是不是都一样?一样的由框架做,不一样的留给应用。
依这条线,框架不碰三块:
- 业务判断:状态怎么转、什么情况不准出货,每一家都不一样
- 报表与批量的 SQL:多表 JOIN、汇总、动态条件、性能调校,硬套定义只会更复杂
- 应用外壳:登录界面、连接设置、导航菜单、窗口与主题
不碰不等于不管。这三块交出去之后,框架反而多出两件必须做好的事:
- 一张注册表,把表单代号对到你写的那个类
- 几个位置明确的扩展点,让你的逻辑挂得进保存与删除的流程
这两件做得好不好,直接决定应用写起来是顺的还是别扭的(第四章展开)。
交出去也不代表复杂度变少。定义能收掉的是重复,收不掉业务本身的难度;那些难的地方没有消失,只是被集中到一处,交给知道它难在哪里的人来写。
第三块在案例里看得最清楚:Northwind 的前端有一个各端共用的 UI 项目,手写的界面只有四个(主窗口、登录、连接设置、承载表单的那一层),全部是外壳。八张业务表单没有一张要写界面程序。
线画在哪里一眼就看得出来:人手写的部分,恰好就是定义没有接管的那一段。
四、架构模式:三个来源,各取一段
前三题答完,这套框架的形状就定了,第四题留到下一节。中间先处理一个常被拿来描述它的说法:N-Tier、Clean Architecture 与 MVVM 的混血。「混血」很容易变成装饰词,所以逐项讲清楚取了什么、没取什么。
| 来源 | 取了什么 | 没取什么 |
|---|---|---|
| N-Tier | 层次边界与跨层数据容器。一次请求走前端 → API → Business Object → Repository → 数据访问基础设施 → 数据库,每层只认相邻那层。三者中取得最完整 | 无 |
| Clean Architecture | 依赖方向向内(第一节那条),翻项目引用清单就能验证 | 对 Domain 的要求。纯粹的做法是每个业务概念一个强类型 Domain Entity,而上千张表逐一建模正是昨天讲的规模成本本身 |
| MVVM | 三个角色一个不缺:Model 是跨层数据容器、View 由布局定义渲染、ViewModel 是框架提供的 DataObject |
无,差别只在 ViewModel 由谁写 |
那个 DataObject 的职责一项不缺:包住数据容器、提供字段读写给界面绑定、发出值变更与未保存状态的通知、管理明细行编辑的开始与取消、向后端加载保存删除。差别是它不逐张手写,形状由定义决定。代价是界面绑定的不再是 order.CustomerId 这种属性,而是一个字段名字。
跨层的数据容器也不是自定义类型,那个选择的代价 Day 9 会专门谈。
五、什么样的系统不该套
进场前先问三个问题,任何一个答不过去就该认真考虑别套。
Q1:你的界面是不是制式表单?
定义能驱动布局的前提是布局模式收敛:目前收敛出来的是主表的字段区块加上明细区块。仪表板、图表、拖拽式调度、地图都不在模式里。框架的回答不是「也支持」,而是这条路不走定义,你自己写。
Q2:你的请求是不是一笔单据?
跨层数据容器是自描述的,代价是每次回应都带着字段结构一起送。一张订单值得,每秒上万笔的事件流不值得。
Q3:你的系统要活多久?
成本集中在前期,回报要三到五年才显现。做完就结案、三年后整套重写的系统,付了前期成本却收不到回报。
三个条件同时成立的场景不多,而企业内部管理系统刚好全中:财务、采购、库存、销售、人力资源、客户关系,界面几乎都是制式表单,请求几乎都是单据,一上线就会活很久。
这些界线不是「框架还没做到」,是做到了会更糟。一套什么都能做的框架,等于什么决定都没替你做,而框架的价值恰恰就在那些决定上。
Northwind 案例:一份定义做了多少事
案例的订单在定义里只有一份表单结构,它交代了:
- 主表配一张明细
- 主表上三处跨表关联(客户、员工、货运商),都是开窗挑一笔回来
- 明细每一行再挑一项产品
- 金额不给人填,由数量、单价与折扣算出来
- 保存前要过三条验证规则
写完这一份,第二节那份清单全部到位:数据表照着建、清单与开窗选取的布局直接长出来、增修删与查询的 SQL 由框架自己组、数据跨层传的时候带着结构一起走、验证规则到保存那一刻会被检查。没有一项要另外写程序。
同一份定义,四个前端一起读:桌面、浏览器、iOS、Android,共用同一个 UI 项目。
框架的价值不在它能做多少,而在它分得清哪些该由它决定:一千次都一样的事归它,每次都不一样的事归你。这条线画歪了两边都是负担,画得太保守,同一件事你要重做一千次;画得太贪心,你要花更多力气去绕过它。
明天让案例正式登场。
本文为 2026 iThome 铁人赛参赛文章,原文为繁体中文。
系列完整目录:ERP 架构师笔记:定义驱动的框架设计
文章摘自:https://www.cnblogs.com/jeff-tsai/p/23462499/day-02-framework-decisions
