Day 2:Definition-Driven 架构总览


昨天结尾说今天要谈框架该替用户决定哪些事。

框架与函数库的差别常被说成「你调用函数库,框架调用你」。这句话描述的是机制,不是代价。真正的代价是控制权反转之后,框架的默认值就变成你的默认值:它决定数据长什么样子、请求怎么流动、扩展点开在哪里。你可以不同意,代价是绕过它,而绕过一套框架通常比不用它更贵。

所以评价一套框架不该问「它能做什么」。能做的事越多,代表它替你决定的事越少,同样的决定,每一张表单都得再做一次。

本篇回答 Definition-Driven 框架必须先定案的四个问题,其余设计全部是这四个答案的推论:

  1. 系统的唯一真相放在哪一层
  2. 哪些层由它衍生
  3. 哪些留给应用自己做
  4. 这一整套在什么样的系统上不成立

这四题没有一题可以留白。框架不回答,用它的人就会各自回答,而且答案彼此不同:这个模块把真相放在 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 铁人赛参赛文章,原文为繁体中文。

iThome 原文 | HackMD 版

系列完整目录:ERP 架构师笔记:定义驱动的框架设计

文章摘自:https://www.cnblogs.com/jeff-tsai/p/23462499/day-02-framework-decisions