软件设计之道-“田”字模型
开篇:《“老王烧烤”的数字化转型》
“兄弟们,大家好。今天我们来聊一个可能会改变我们工作习惯的新模型。在开始讲枯燥的理论之前,我先给大家讲个故事,关于咱们公司楼下那家‘老王烧烤’。”
第一部分:老王的传统手艺(过去的我们)
“老王烤得一手好羊肉串,以前生意全靠他一个人。客人来了,冲他喊:‘王哥,来一手肥点的,多辣!’ 老王心里记下,转身就从冰箱里拿肉,现穿现烤,最后吆喝一嗓子:‘您的串好嘞!’,亲自端上去。
这个过程,简单直接,但问题很大:
- 全凭脑子记: 人一多,谁点了啥、要不要葱花香菜,根本记不住,老上错菜。
- 效率极低: 他一个人又当点单员、又当厨师、又当传菜员,忙得脚不沾地,一晚上也接待不了几桌。
- 无法规模化: 老王病了或者想开分店,这套手艺根本复制不了。
这像不像我们过去的某些项目?
- 需求全靠产品经理口头传达。
- 一个高手程序员从头写到尾,又是前端又是后台又是数据库。
- 代码耦合严重,只有他自己能维护。
- 项目无法复用,再来一个类似的需求,还得重头再写一遍。
第二部分:老王的数字化危机(我们遇到的痛点)
“后来老王上了‘美团’,生意爆火,订单蜂拥而至。问题也更大了:
- 输入不标准: 有客人备注‘微辣’,有写‘一点点辣’,有写‘辣度一级’。老王懵了,这‘一点点’到底是多少?
- 处理不过来: 订单打印出来贴了一墙,老王根本看不过来,经常烤糊或者漏单。
- 输出会出错: 外卖小哥挤在门口问:‘订单尾号6789是谁的?’,店员手忙脚乱地找,经常拿错。
这像不像我们系统间的混乱交互?
- 上游系统传过来的数据格式千奇百怪,我们要写一大堆
if...else来适配。 - 系统内部逻辑复杂,一量上来就性能瓶颈,频繁宕机。
- 输出的接口、文档混乱,调用方抱怨连连,联调成本极高。
第三部分:老王的智慧转型(“田”字模型的精髓)
“老王痛定思痛,花了点时间做了一个革命性的改革:
flowchart TD
A[顾客下单] --> B[统一输入<br>标准点单码]
B --> C[标准化处理<br>中央厨房]
C --> D[统一输出<br>标准化出餐]
subgraph A [第一横:输入标准化]
B
end
subgraph B [第二横:处理专业化]
C
end
subgraph C [第三横:输出标准化]
D
end
B --> C
C --> D
-
【定义输入 - 第一横】:他搞了个标准化点单码。每个菜品都有一个唯一码,辣度固定为【不辣、微辣、中辣、特辣】四档。客人只能这么选。从源头上保证了输入的统一和清晰。
-
【优化处理 - 第二横】:他在后院搭了个中央厨房。前厅店员只负责接单,把标准订单传给后厨。后厨分工明确:有人专门穿串,有人专门掌火,有人专门刷酱。各司其职,专业高效。
-
【明确输出 - 第三横】:他设立了统一的出餐口。每个烤好的套餐都用一个标有订单号的盘子装好,放在出餐台上。外卖小哥来了,直接报号取餐,又快又准。
“改革之后,老王的生意做到了这条街第一。”
第四部分:从烧烤到代码(我们的启示)
“兄弟们,我们写代码、做系统,和老王开烧烤店,本质是一模一样的!”
- 那个 ‘标准化点单码’,就是我们的输入规范和接口契约。我们必须先和上游定义清楚,他们到底‘喂’什么数据给我们。
- 那个 ‘中央厨房’ 和专业分工,就是我们的应用内部设计。我们要把自己的系统拆分成一个个职责单一的模块(比如订单服务、库存服务、支付服务),而不是一个什么都干的‘大泥球’。
- 那个 ‘统一的出餐口’ 和订单号,就是我们的输出结果。我们给前端、给其他系统提供的数据和接口,必须标准、清晰、稳定。
“如果我们不学老王,而是在美团订单涌来时,还在那手忙脚乱地靠脑子记‘一点点辣’,那结局只能是系统崩溃,全员加班,客户投诉。”
“所以,我们今天要介绍的这套 ‘田’字模型,其实就是把我们代码世界的‘中央厨房’给建起来。”
- 第一横:输入 -> 搞清楚 ‘谁给我喂数据?’ (定义好你的点单码)
- 第二横:处理 -> 想清楚 ‘我拿到数据后要干什么?’ (设计好你的中央厨房和流水线)
- 第三横:输出 -> 明确 ‘我干完活要交出什么?’ (准备好你的出餐口)
“从今天起,我希望大家在动手写代码前,都像老王设计他的中央厨房一样,先花时间做好设计。先定义规矩,再开始干活。这样,我们才能从‘手工作坊’式的开发,转变为‘现代化工厂’式的生产,最终交付稳定、高效、可扩展的系统!”
“好,接下来,我们看看具体该怎么操作……”
一、【功能名称】功能边界定义表
1.1 功能概述
- (用一两句话描述这个功能是干什么的)
1.2 输入 (Input) - “谁给我喂数据?”
指引: 列出所有触发此功能执行或为此功能提供数据的来源。思考:数据从哪来?是什么格式?怎么传给我?
1.2.1 上游调用者/数据来源
| 来源类型 | 来源方(哪个应用/用户) | 触发方式(API调用/消息事件/定时任务) | 协议与数据格式(HTTP/Event, JSON/?) | 核心数据字段说明 |
|---|---|---|---|---|
| 例如:内部应用 | 前端Web页面 | HTTP POST API调用 | application/json | { "productId": 123, "quantity": 2 } |
| 例如:消息队列 | 订单服务 | 监听 order.created 事件 | JSON | { "orderId": "O001", "userId": "U001" } |
| ... | ... | ... | ... | ... |
1.2.2 必要的前置条件与校验规则
- 权限校验: 调用者需要具备什么角色/权限?
- 数据校验: 输入数据需要满足哪些业务规则?(如:库存是否充足?)
- 幂等性要求: 是否需要防止重复提交?Token是什么?
1.3 输出 (Output) - “我干完活要交出什么?”
指引: 列出这个功能执行后所产生的所有结果和副作用。思考:我要返回什么?要通知谁?要改变什么状态?
1.3.1 直接响应
| 输出类型 | 消费者(返回给谁) | 输出方式 | 数据格式 | 核心数据字段说明 |
|---|---|---|---|---|
| 同步响应 | 前端Web页面 | HTTP Response | application/json | { "orderId": "O001", "status": "created" } |
| ... | ... | ... | ... | ... |
1.3.2 异步输出与副作用
| 输出类型 | 消费者(通知给谁) | 输出方式 | 数据格式 | 核心数据字段说明 |
|---|---|---|---|---|
| 领域事件 | 积分服务、消息推送服务 | 发布 OrderCreatedEvent 消息 | JSON | { "orderId": "O001", "userId": "U001" } |
| 数据库变更 | - | 写入 orders 表 | - | 生成订单数据 |
| ... | ... | ... | ... | ... |
1.4 处理 (Process) - “我拿到数据后要干什么?”
指引: 在厘清输入输出后,基于它们来设计核心业务逻辑。这会让你思路更清晰。
1.4.1 核心业务流程
flowchart LR
A[接收输入请求] --> B{校验权限与数据}
B -- 校验失败 --> C[返回错误信息]
B -- 校验成功 --> D[执行核心业务逻辑-创建订单,扣减库存]
D --> E[持久化数据到数据库]
E --> F[发布订单创建事件]
F --> G[返回成功响应]
1.4.2 关键领域模型与服务
- 主要参与的模型:
Order(订单),Product(商品) - 核心服务/方法:
OrderService.createOrder()
1.5 讲清楚
- 产品经理: ________ 我已确认上述输入、输出及业务逻辑符合需求定义。
- 测试工程师: ________ 我已基于此文档开始设计测试用例,确认覆盖所有输入输出场景。
- 技术负责人/架构师: ________ 设计合理,同意进入开发阶段。
- 开发工程师: ________进入开发
二、核心理念:像搭积木一样构建应用
首先,告诉开发团队,我们要改变“接到需求就建表写CRUD”的习惯。每一个应用或功能模块都应该被看作一个有明确边界、有输入输出、可复用的“积木”(或称为“组件”、“服务”)。
这个“积木”的内部运作,必须遵循“田”字模型:
flowchart TD
subgraph SecondHorizontal [第二横:我们的应用/服务-处理]
direction LR
Process[内部业务逻辑<br>与数据处理]
end
Input[第一横:输入<br>上游应用/数据/事件] --> Process
Process --> Output[第三横:输出<br>页面/接口/事件/数据]
subgraph Roles [三竖角色-贯穿始终]
direction TB
R1[产品-定义做什么]
R2[技术-设计怎么做]
R3[用户/测试-验证好不好用]
end
Input -.-> Roles
Process -.-> Roles
Output -.-> Roles
架构师的指令:
“兄弟们,以后做任何一个新功能,先别敲代码。第一步,先把它框起来,问自己三个问题:
- 谁给我喂数据? (Input)
- 我拿到数据后要干什么? (Process)
- 我干完活要交出什么? (Output)
把这三个问题和产品、测试聊清楚,形成文档,我们再开始设计。”
三、开发流程:用清单驱动设计
接下来,为团队引入“清单文化”,将“田”字模型的抽象思维转化为具体的开发任务和设计文档。这个流程及其产出物如下图所示:
flowchart LR
A[需求评审<br>PRD/原型] --> B[第一步:创建功能场景清单]
B --> C[第二步: 细化输入,处理,输出清单]
C --> D[第三步: 推导出数据库与集成设计]
D --> E[第四步: 架构师评审清单]
E -- 通过 --> F[编码实现]
E -- 驳回 --> B
subgraph B [第一步:创建功能场景清单]
direction LR
B1[梳理所有场景<br>明确每个场景的I-P-O]
end
subgraph C [第二步: 细化输入,处理,输出清单]
direction LR
C1[输入清单]
C2[处理清单]
C3[输出清单]
end
subgraph D [第三步: 推导出数据库与集成设计]
direction LR
D1[数据库设计清单]
D2[集成清单<br>接口,事件,菜单]
end
架构师的指令:
“我们的开发流程要增加一个设计环节。每个人都要负责填写这几张表:
1. 功能场景清单 (I-P-O清单):
场景编号 场景名称 输入 (Input) 处理 (Process) 输出 (Output) C001 用户下单 商品ID、用户ID、收货地址 1. 校验库存<br>2. 计算价格<br>3. 创建订单 订单ID、支付链接 ... ... ... ... ... 2. 输入/处理/输出详细清单: 对每个场景的I、P、O进行细化。 3. 数据库设计清单: 表中的每个字段都必须能在上述清单中找到来源和目的。 4. 集成清单: 明确需要暴露哪些接口、监听哪些事件。
我会评审这些清单,通过之后才能开始写代码。这是我们的‘开工许可证’。”
四、设计目标:打造“可复用积木”,而非“一次性烟囱”
教导团队如何识别和设计“可复用积木”。关键在于分析“输入”的多样性,并在“处理”层做好抽象
flowchart TD
subgraph A [不可复用的烟囱式设计-避免]
A1[App 1] --> B1[定制化服务A<br>仅理解App1的数据]
B1 --> C1[输出]
end
subgraph B [可复用的积木式设计-追求]
direction TB
D1[App 1] --> E
D2[App 2] --> E
D3[App 3] --> E
subgraph E [通用服务-我们的应用]
E1[通用输入适配层<br>参数配置/规则模板/数据转换]
E2[核心业务处理层<br>保持稳定]
end
E --> F[通用输出层<br>接口/事件/页面]
F --> G[其他应用]
end
架构师的指令:
“我们要做的不是一个只能接一种数据、服务一个应用的‘烟囱’。我们要做的是一个万能适配器。
比如设计‘证书服务’,不要只想着从‘培训系统’接数据。要把‘输入’设计得足够通用:
- 用‘证书模板’来定义不同类型的证书。
- 用‘参数配置’来适配不同来源的数据字段。
- 用‘流程编排’来满足不同客户的不同审核流程。
这样,将来‘荣誉系统’、‘考核系统’想发证书,直接配置一下就能接入,不需要我们二次开发。这就是我们工作的价值所在!”
五、作为架构师,你要告诉开发
- 转变思维: 从“实现功能”转变为“设计积木”。每个功能都要先定义清其I-P-O边界。
- 规范流程: 推行“清单驱动开发”,将设计过程文档化、可视化,并将其作为编码的前置条件。
- 明确目标: 以“可配置”、“可复用”为最高追求之一,通过抽象输入、通用化处理,将系统打造成平台型产品,而非项目型定制。
- 质量把关: 用“九性原则”去评审清单和代码,确保性能、安全、可扩展等非功能需求得到满足。
六、落地实施四件套:流程、模板、评审、度量
我们将通过四个具体的抓手来确保开发按照“田”字模型进行设计。
6.1 抓手一:定义强制性的设计阶段流程
将“设计阶段”正式纳入开发流程,并设置为编码的前置关卡。可以使用类似如下的流程图来明确告知团队:
flowchart TD
A[需求评审会] --> B{是否新功能/新应用?}
B -- 是 --> C[启动-田字模型设计阶段]
B -- 否<br>简单功能/bug修复 --> G[走简化流程或直接开发]
subgraph C [设计阶段核心任务]
C1[任务1: 撰写<功能场景清单>]
C2[任务2: 撰写<输入输出清单>]
C3[任务3: 撰写<领域模型清单>]
C4[任务4: 产出<架构设计说明书>]
end
C -- 设计文档完成后 --> D[召开-设计评审会]
D -- 评审通过 --> E[架构师在OA任务/JIRA上签字]
E --> F[开发团队凭签字<br>开始编码]
D -- 评审不通过 --> C
告诉开发:
“兄弟们,新流程来了。以后所有新需求,在PRD评审后,必须先在JIRA上完成【设计阶段】的任务,产出设计文档并通过评审,这个JIRA任务的状态才能变为【开发中】。这是硬性规定,没有例外。”
6.2 抓手二:提供标准化模板
创建模板,让开发“填空”,引导他们按“田”字模型思考。这是最重要的落地工具。
模板示例:《XXX功能/应用架构设计说明书》
6.2.1 功能场景清单 (I-P-O)
- 说明: 拆解PRD,列出所有主要功能点,并用一句话说清每个功能的输入、处理和输出。
| 场景编号 | 场景名称 | 输入 (Input) | 处理 (Process) | 输出 (Output) |
|---|---|---|---|---|
| C001 | 用户提交订单 | 商品ID、SKU、数量、用户ID、地址 | 1. 校验库存<br>2. 计算价格(含优惠券)<br>3. 创建订单数据 | 1. 订单创建成功页面<br>2. 创建订单事件 |
| ... | ... | ... | ... | ... |
6.2.2 输入清单
- 说明: 详细定义每个输入的来源、格式和约束。
6.2.2.1 内部输入(来自其他应用)
| 输入编号 | 对应场景 | 来源应用 | 接口/事件名 | 数据格式 | 关键字段说明 |
|---|---|---|---|---|---|
| I001 | C001 | 商品中心 | GET /api/product/{id} | JSON | stock: integer |
| I002 | C001 | 优惠券中心 | POST /api/coupon/validate | JSON | valid: boolean, discount: number |
6.2.2.2 外部输入(来自用户/第三方)
| 输入编号 | 对应场景 | 输入方式 | 数据格式 | 校验规则 |
|---|---|---|---|---|
| E001 | C001 | HTTP POST | JSON | address字段不能为空 |
6.2.3 处理清单(核心业务逻辑)
- 说明: 对“处理”部分进行详细设计,这是领域建模的核心。
6.2.3.1 领域模型设计
| 模型名 | 职责 | 核心属性 | 是否根实体 |
|---|---|---|---|
| Order | 订单聚合根,负责订单生命周期 | orderId, userId, totalAmount, status | 是 |
| OrderItem | 订单项 | productId, quantity, price | 否 |
6.2.3.2 关键业务流程设计
6.2.4 输出清单
- 说明: 定义所有输出,明确消费者是谁。
6.2.4.1 接口输出
| 输出编号 | 对应场景 | 接口路径 | 消费者 | 数据格式 |
|---|---|---|---|---|
| O001 | C001 | POST /v1/orders | 前端 | JSON |
6.2.4.2 事件输出
| 输出编号 | 事件名称 | 对应场景 | 消费者 | 目的 |
|---|---|---|---|---|
| EV001 | OrderCreatedEvent | C001 | 支付中心、积分中心 | 触发后续流程 |
6.2.5 数据库设计
- 说明: 这里的表设计必须源自上面的领域模型和清单。
| 表名 | 存储内容 | 与场景编号关联 | 主要字段 |
|---|---|---|---|
order | 订单主数据 | C001 | id, user_id, amount, status |
order_item | 订单商品数据 | C001 | order_id, product_id, quantity |
6.2.6 非功能性设计(九性原则考量)
| 原则 | 中文释义 | 核心问题 | 设计策略/考量点 |
|---|---|---|---|
| Performance | 性能 | 系统响应快慢?吞吐量如何? | 响应时间、吞吐量、资源利用率、缓存、异步、池化、CDN、数据库优化 |
| Reliability | 可靠性 | 系统出故障的频率高吗? | 平均无故障时间(MTBF)、错误处理、容错、冗余、自动恢复 |
| Availability | 可用性 | 系统在多长时间内可供使用? | 故障时间占比(如99.99%)、冗余、故障转移、负载均衡、优雅降级 |
| Security | 安全性 | 系统能否防止恶意攻击和未授权访问? | 认证、授权、审计、加密、防注入、防篡改、漏洞管理 |
| Modifiability | 可修改性 | 系统容易修改吗?修改成本高吗? | 模块化、解耦、高内聚低耦合、接口抽象、微服务架构 |
| Portability | 可移植性 | 系统能轻易换一个环境运行吗? | 消除对OS、平台、数据库的依赖,使用容器化技术(Docker) |
| Reusability | 可复用性 | 系统的部分功能能轻易被其他项目使用吗? | 组件化、服务化、清晰的接口设计、通用库、设计模式 |
| Integrability | 可集成性 | 系统能容易地与其它系统对接吗? | 标准化接口(RESTful API、GraphQL)、通用数据格式(JSON)、提供SDK |
| Testability | 可测试性 | 系统容易测试吗? | 单元测试、依赖注入、 mocking、日志、健康检查接口 |
如何在实际设计中应用这九性?
在你的《设计说明书》中,“非功能性设计”部分就应该围绕这九性来展开。以下是如何将它们具体化的示例:
- 1. 性能 (Performance):
- 指标: 创建订单接口P99 < 200ms。商品列表查询接口支持 1000 QPS。
- 设计: 使用Redis缓存商品信息。对数据库查询建立索引。异步处理非核心逻辑(如发送通知)。
- 2. 可靠性 (Reliability) & 3. 可用性 (Availability):
- 指标: 系统整体可用性达到99.95%(全年故障时间不超过4.4小时)。订单数据零丢失。
- 设计: 数据库采用主从复制。服务部署至少2个实例,并配置负载均衡。关键操作(如扣库存)必须具备幂等性。
- 4. 安全性 (Security):
- 指标: 无高危安全漏洞。
- 设计: 所有接口需通过网关校验JWT令牌。实施RBAC权限模型。对用户输入进行严格校验和过滤,防止SQL注入和XSS攻击。敏感信息(如密码)加密存储。
- 5. 可修改性 (Modifiability) & 6. 可复用性 (Reusability):
- 指标: 新增一种支付方式,开发周期不超过3人/日。
- 设计: 采用策略模式设计支付模块。将订单核心领域与支付、物流等外部能力解耦,定义清晰的接口。力争将“支付能力”提炼为可复用的公共组件。
- 7. 可移植性 (Portability):
- 设计: 使用Docker容器化部署,消除对环境依赖。所有配置外部化,可通过配置中心管理。
- 8. 可集成性 (Integrability):
- 设计: 提供清晰的RESTful API文档(使用Swagger/OpenAPI)。为前端提供便捷的SDK。定义明确的事件契约(如
OrderCreatedEvent),方便其他系统订阅。
- 设计: 提供清晰的RESTful API文档(使用Swagger/OpenAPI)。为前端提供便捷的SDK。定义明确的事件契约(如
- 9. 可测试性 (Testability):
- 设计: 代码遵循依赖倒置原则,便于注入Mock进行单元测试。提供
/health端点供健康检查。生成充足的日志,便于集成测试和线上问题排查。
- 设计: 代码遵循依赖倒置原则,便于注入Mock进行单元测试。提供
6.3 抓手三:设立正式的设计评审会
流程和模板有了,就需要一个仪式来审视成果。
- 会议名称: “架构设计评审会” 或 “技术方案评审会”。
- 参会人: 架构师、该项目的开发人员、主测、感兴趣的其他开发。
- 评审依据: 就是上面要求提交的《架构设计说明书》。
- 评审核心问题:
- 完整性: 三横(I-P-O)是否都覆盖了?清单是否齐全?
- 清晰性: 设计是否清晰?其他人是否能看懂?
- 可复用性(工具化): 输入是否抽象得足够通用?是不是又是“烟囱式”设计?能否通过配置应对未来类似需求?
- 可行性: 技术实现上有无难点?依赖的服务是否可靠?
- 质量: 是否考虑了“九性原则”(如性能、可靠性、可用性、安全性、可修改性、功能性、可变性、互操作性、易用性)?
告诉开发:
“设计文档写完后,预约评审会。在会上,你需要对着你的文档给大家讲清楚你的设计。我们会重点问上面这几个问题。评审不通过,打回去重新设计,直到通过为止。”
6.4 抓手四:度量与持续改进
如何知道这个方法论是否成功?需要度量。
-
量化指标:
- 设计阶段平均耗时: 从PRD评审到设计评审通过的时间。初期会变长,后期应稳定并缩短。
- 返工率: 因设计缺陷导致的代码返工比例。这个指标应该下降。
- 需求吞吐量: 理论上,因为前期设计得好,后期编码调试更顺,整体吞吐量应上升。
-
质性反馈:
- 定期(如每季度)收集开发和产品的反馈:“你觉得新的设计流程有帮助吗?有什么痛点?”
- 收集“因为前期设计好而避免了后期大坑”的成功案例,在团队内部分享,增强大家的认同感。