四讲干了一件事:把一个 725 行的 mock 服务端,改造成一个"能上线"的服务端。
然后回头看——改动落在了哪里,以及每一次改动的代价是什么。★ 而最值得先看的是一件反直觉的事:四讲下来,业务逻辑只多了 29 行。
一 · 四轮改造,一张表
| 讲 | 改造目标 | 被逼出来的东西 | 代价落在哪 |
|---|---|---|---|
| 41 | 换掉手写的 HTTP 层 | RPC 框架(restrpc)· httptest DSL · 两层分层的确立 | 协议层 286 → 169 业务层零改动 第一次有了外部依赖(3 个) |
| 42 | 换掉内存,接上数据库 | 多租户(uid)· mongodb · 表与索引 · 详细设计文档模板 · mock 授权 | 业务层 330 → 359(几乎全部重写) 协议层 169 → 230 测试 89 → 179 |
| 43 | 想清楚"授权的可信性从哪来" | 帐号 / 授权 / Session vs Token / OAuth 2.0 | 零行代码(纯概念) |
| 44 | 把可信性接上 | 网关:验 JWT → 删 Cookie → 换内网协议 | 业务层零改动 网关 35 → 192 第 5 个外部依赖 |
★ 注意 41 讲和 44 讲的共同点:业务层零改动。
一次是换掉整个 RPC 框架,一次是接入整套帐号授权体系——业务层都不知道发生了什么。
二 · 四个分支的真实行数
v31 v41 v42 v44-bear
(mock) (restrpc) (mongo) (网关)
─────────────────────────────────────────────────────────────
业务层 drawing+shape 330 → 330 → 359 → 359 ×1.09
协议层 service.go 286 → 169 → 230 → 230 ×0.80
测试 两个 _test 109 → 89 → 179 → 179 ×1.64
─────────────────────────────────────────────────────────────
paintdom 合计 725 → 588 → 768 → 768 ×1.06
网关 paintweb/main 35 → 35 → 35 → 192 ×5.49
外部依赖(go.mod) 0 → 3 → 4 → 5
2.1 ★ 第一行最值得盯着看:业务逻辑只多了 29 行
四讲下来,drawing.go + shape.go 从 330 行变成 359 行。
而这四讲干的事是:换 RPC 框架、加多租户、换数据库、加帐号授权。
★ 这就是 41 讲那个"业务逻辑实现层 / RESTful API 层"两层分层的全部回报。
而它和前端五讲那组数字(
dom.js109 → 856,×7.9)指向的是同一件事的两面:前端五讲:需求在【长】,Model 就该跟着长(×7.9),而 Controller 不该被打扰(×0.95) 后端四讲:需求在【换环境】,业务层就【一行都不该动】(×1.09)★ 合起来是一句话:业务的变化落在业务层,环境的变化落在环境层。
一个分层是不是真的成立,看的就是"变化有没有落在它该落的地方"。
2.2 第二行的形状很有意思:协议层先变少,再变多
286 → 169 引入框架,砍掉 117 行样板(41 讲)
169 → 230 加回 61 行:自定义 Env(授权解析)+ 每个方法多一个 env.UID(42 讲)
★ 加回来的那 61 行,性质和砍掉的 117 行完全不同:
是什么 砍掉的 117 行 纯样板——取参数、解 body、序列化、翻错误码,同一件事重复 8 遍 加回的 61 行 真业务约束——“每个请求必须知道是谁在调” 框架能替你消掉的只有第一种。 第二种(横切的业务约束)只能你自己写,
但框架给了它一个正确的落点(OpenEnv),于是它只写一遍而不是八遍。
2.3 第三行:测试翻了一倍,而且是被"约束"逼出来的
109 → 89 41 讲:用 DSL 重写,同样的覆盖度更少的行
89 → 179 42 讲:★ 新增 6 个序列化测试 + 更完整的业务链路测试
42 讲新增的那 6 个测试全是同一个形状(带读 16 · 2.7):
TestPathEncode json.Marshal(val) 必须等于 `{"id":"","path":{…}}`
TestPathBsonEncode bsonMarshal(val) 必须等于 `{"id":"","path":{…}}` ★ 同一个字符串
★ 它们把 README_IMPL 里那两条"人话约束"变成了可执行的东西:
“json.Marshal结果必须符合 API 层预期” + “bson.Marshal结果必须符合 mongodb 预期”。★ 一条判断:一个约束如果只写在文档里,它就会在某次重构里悄悄失效。
写成测试,它才真的存在。
2.4 最后一行:依赖从 0 涨到 5
v31 0 个
v41 3 个 qiniu/http(restrpc)· qiniu/qiniutest · qiniu/x(mockhttp)
v42 4 个 + gopkg.in/mgo.v2
v44 5 个 + dgrijalva/jwt-go
★ 这条曲线就是整个后端篇的隐含主线(第三节 ① 那条):
每引入一个依赖,就是把一件"跟画图无关的事"交出去。⚠ 但代价要记账(带读 15 · 第五节):
依赖不是"引入了一个库",是"引入了一个别人的发布节奏"。
而这四讲选的五个依赖里,今天有三个已经不该用了——见第六节的诚实清单。
三 · 反复出现的六个手法
① ★ 把"跟业务无关的部分"一层层交出去
这是后端四讲唯一的主线,比任何具体技术都重要。
| 讲 | 交出去了什么 | 交给了谁 | 剩下的 |
|---|---|---|---|
| 41 | HTTP 协议的解析与序列化 | restrpc | — |
| 41 | 测试的样板 | httptest / mockhttp | — |
| 42 | 数据的持久化、并发控制、唯一性约束 | mongodb(+ 唯一索引) | — |
| 43/44 | 帐号、密码、登录、授权、令牌 | dex / JWT / 网关 | — |
★ drawing.go + shape.go,359 行 |
★ 那 359 行才是 QPaint。四讲的全部工作,是把它周围的东西一件件搬走。
★ 而这正好回答了 41 讲开头那个观察:
“服务端开发,难在基础软件很多,对程序员和架构师的知识面和理解深度都有较高的要求。
但从业务复杂性来说,服务端的业务逻辑相对简单。”它的解法不是"学会自己写这些基础软件",是【学会认出哪些不该自己写】。
而"认出"需要的恰恰是知识面和理解深度——因为你必须懂它,才知道该不该自己写、
以及交出去之后代价落在哪。
② 每次只动一层
41 讲 只动 service.go(协议层)+ 测试 drawing.go 字节相同
42 讲 主要动 drawing.go(业务层实现) 协议层只加一个 uid
44 讲 只动 paintweb(网关) paintdom 全部文件字节相同
★ 这不是"改动小",是"分层在起作用"的可观测证据。
如果 41 讲要动 6 个文件、44 讲要改业务代码,那说明分层只存在于目录结构里。★ 一个能直接用的自查:下次做一次大改造之前,先预测"应该改哪些文件",
改完对一遍。对不上,就是分层的位置有问题——而这比改动本身更值得关注。
③ mock 一个东西,是为了先把接缝定下来
| 讲 | mock 了什么 | 当时的实现有多假 | 后来怎么兑现 |
|---|---|---|---|
| 29 | 整个服务端 | 全内存,进程一停全没 | 42 讲换成 mongodb |
| 41 | restrpc.Env |
空壳,一点没用 | 42 讲塞进 UID |
| 42 | 授权 | 浏览器自己声称 QPaintStub 1 |
★ 44 讲:格式一字未改,改成网关来写 |
★ 三次都是同一个手法:先把"这件事发生在哪一层、结果长什么样"钉死,
实现给最笨的那个。接缝定对了,换实现就是局部替换。★ 而 42 讲那个 mock 授权是最好的样本:它是"任何人都能伪造"的,
看起来毫无价值——但它定下的那个接缝(env.UID)原封不动活到了最后,
于是真授权来的时候,业务服务连重新编译都不需要。
④ ★ 声明式的约束 > 每次记得检查
这一招在四讲里出现了三次,每次都是同一个形状:
| 讲 | 原来 | 换成 |
|---|---|---|
| 42 | if _, ok := shapes[id]; ok { return EEXIST } + mutex |
唯一索引(声明一次,所有写入路径自动受约束) |
| 42 | Get(dgid) 然后 if d.uid != uid { 403 } |
把 uid 写进查询条件(想泄漏也泄漏不了) |
| 44 | 业务服务自己也检查一下 cookie | 网关删掉 Cookie(业务服务根本看不到) |
┌────────────────────────────────────────────────────────────────┐ │ ★ 通用手法:把"要记得做的事"变成"不做就编不过 / 查不到 / 看不到"。│ │ `if` 只保护写了它的那一条路径; │ │ 声明式约束保护【所有路径】,包括你还没写的那些。 │ └────────────────────────────────────────────────────────────────┘
⑤ 按"频率"把矛盾的需求拆成两个
| 讲 | 拆什么 | 高频那个的特权 |
|---|---|---|
| 37 | 读 / 写 | 读可以走从库 |
| 39 | 热 / 冷 | 热的走内存 |
| 43 | Access / Refresh token | ★ 高频那个不许查库(于是无状态,于是不能吊销) |
★ 通用形式:当一个东西要同时满足两个互相矛盾的要求时,
先看能不能按【频率】拆成两个,让高频那个只承担便宜的要求。
⑥ 不能消除失败,就选择失败的样子
| 讲 | 不能消除什么 | 怎么选 |
|---|---|---|
| 42 | 多步修改中途宕机(没有事务) | 先建被指的、后建指针;先删指针、后删被指的——让残留物是"看不见的垃圾"而不是"看得见的错误" |
| 43 | 无状态 token 不能吊销 | 让它短命——短命是无状态系统唯一的吊销机制 |
| 39 | 缓存与存储不可能原子 | 接受不一致,用 TTL 把"永久错"降成"错 5 分钟" |
★ 三条是同一条:工程里很多问题没有"解决",只有"降级到可接受的形状"。
而"选哪个形状"往往是免费的(换个顺序、改个数字),比真解决便宜几个数量级。
四 · 十六条判断(可以单独当清单用)
分层
- 一个函数的可测性 ≈ 它的签名里有多少"环境对象"。(带读 15 · 2.3)
- 分层是否成立有一个可观测指标:API 层 handler 的平均行数。 5 行以内大概是真的,50 行说明"分层"只在目录结构里。(15 · 3.3)
- 网络协议是【一种】使用方式,不是【唯一】使用方式。 业务逻辑写死在协议里,就永久失去了"不走网络地用它"的能力。(15 · 3.2)
- 分层的失守往往不是从"写错"开始的,是从"绕过去"开始的。(15 · 3.4)
- 凡是"将来可能换"的东西都是环境;凡是"换了它业务还是那个业务"的都是业务。(15 · 3.6)
数据与存储
- "存储即数据结构"有两半:选了存储 = 选定能高效做什么 + 放弃一批数据结构(指针、链表、树在存储里没有对应物——
SetZorder就是这么丢的)。(16 · 三 · 四) - 进程内的互斥(mutex / Lock / GIL)在多台机器上一律失效。 靠它保住的每个不变量都要重新找分布式的靠山。(16 · 3.5)
- 联合索引的字段顺序由"要跑哪些查询"决定(最左前缀),不是反过来。(16 · 3.4)
- 权限是一个属性,不是一层结构。 把权限编码进结构,结构就会随权限模型一起变——而权限模型是最爱变的东西。(16 · 2.2)
- 类型一旦要跨出进程(存盘、上网、进队列),字段名就变成对外契约的一部分。改一个字段名 = 一次数据迁移。(16 · 2.6)
安全与授权
- 租户条件应该出现在 filter 里,不该出现在 if 里——因为 403 和 404 的差别会泄漏"这个 ID 存在吗"。(16 · 2.4)
- 少传密码有两个独立理由:泄漏面(安全)+ 密码校验被故意设计成慢的(性能,实测差 4 万倍)。(17 · 一)
- 一个协议的可信性不来自它的格式,来自"谁被允许生成它"。(18 · 1.2)
- 不要让【被验证的数据】决定【怎么验证】(
alg:none、算法混淆、pickle 反序列化——同一条)。(18 · 六) - 网关的职责不只是"翻译外部凭据",还包括"把外部凭据彻底拿掉"。(18 · 7.1)
- 权限的粒度不是技术问题,是"人能不能在三秒内做出正确决定"的问题。(17 · 八)
五 · 一个模板:详细设计文档的四段
42 讲真正的产出不是 mongodb,是 v42 新增的那份 README_IMPL.md(153 行)。
① 逻辑 DOM 结构 这个系统里【有什么】—— 一棵树
② 使用界面(接口) 每个东西【能被怎么用】—— 方法签名 + 语言之外的约束
③ 数据结构 它【存在哪、长什么样】—— 表、字段、索引
④ 实现逻辑(算法) 每个用户故事【怎么走完】—— 用一种【可执行的语言】写伪代码
★ 顺序不能反,因为每一步给下一步定题:
①→② 接口只能围绕这些实体写;②→③ 表和索引由"要跑哪些查询"决定;
③→④ 算法只能用存储支持的操作拼出来。反过来做(先选数据库再想接口)会得到一类很典型的系统:
接口长得像表的 CRUD,业务语义全在调用方那边。★ 而这份文档正好是 45 讲"怎么做详细设计"的实物样本——42 讲先做了一遍给你看。
六 · 诚实清单
6.1 代码层面(我 diff 分支才发现的)
| # | 事实 |
|---|---|
| 1 | 原文 44 讲让你看的 compare/v42...v44 是空的(0 commit,v44 = v42)。接 dex 的代码不存在。 |
| 2 | 真正有代码的是 v44-bear(原文一开始就说"但我们要 OAuth 所以不这么做"的那条路) |
| 3 | 42 讲把 SetZorder 弄丢了——换存储之后它是 return errNotImpl。原文一个字没提 |
| 4 | 42 讲的 Add 用 $push,它不幂等——重试一次,shapes 数组里就有两个相同的 spid |
| 5 | 42 讲没有用事务(mgo 驱动做不到),靠的是操作顺序。而原文说"应该以事务形式来做" |
| 6 | v44-bear 里 isExpired() 是 return false——一个空实现;login() 里密码 = 用户名;签名密钥是源码字面量 |
| 7 | v42 的浏览器端 Authorization: QPaintStub 1 是硬编码的——多租户在安全上等于零 |
★ 第 3、4、5、6 条都不是"作者写错了",是 DEMO 的合理取舍。
但它们说明了一件事:读实战代码必须去看真实的 diff,正文说的和代码做的不总是同一件事。
而这四条恰好都在同一个位置——“换环境之后,原来靠环境提供的保证去哪了”。
6.2 时间层面(2019 → 2026)
| 原文当时 | 今天 |
|---|---|
mgo.v2 |
社区版 2018 年就停更了,官方驱动是 mongo-go-driver |
dgrijalva/jwt-go |
★ 已废弃(作者停止维护,有 CVE),继任者是 golang-jwt/jwt |
DropDups: true(建唯一索引时自动删重复) |
MongoDB 3.0 就移除了——"自动删数据"从来不该是一个索引选项 |
| mongodb 没有跨集合事务 | 4.0(2018)有了,但分片下依然要 2PC,等于给每次写加一笔税 |
| 六种 OAuth 授权模式 | OAuth 2.1 移除 Implicit 和 Password 模式、强制 PKCE |
| 选 mongodb 因为"Schema 开放" | Postgres 的 JSONB 也能 schemaless,还能对里面的字段建索引 |
| dex 是"唯一想得一样的" | 今天还有 Keycloak / Ory / Zitadel / Authentik,以及 SaaS(Auth0 / Clerk / WorkOS / Logto) |
| CoreOS 团队 | 已被 Red Hat 收购、品牌停用;dex 现在是 CNCF sandbox 项目,仍在维护 |
| dex 不支持微信/支付宝 | 依然不支持,而且原因是微信的"OAuth"不是标准 OAuth 2.0 |
★ 但四讲的判断本身一条都没过时——过时的全是选型,而选型从来不是这四讲要教的东西。
反倒有一条变得更强了:44 讲当年只能在"自己写"和"部署一个开源项目"之间选,
今天多了一档"连部署都不做"(SaaS)——而判据完全没变,还是"这件事与业务无关吗"。
七 · 与前端五讲的对照
两组实战问的是不同的问题,而答案指向同一件事。
| 前端五讲(26~30) | 后端四讲(41~44) | |
|---|---|---|
| 驱动力 | 需求在长(选中、离线、同步) | 环境在换(框架、数据库、授权) |
| 核心问题 | 什么该放进 Model?(Model 层的厚度) | 什么不该自己写? |
| 最硬的数字 | dom.js 109 → 856(×7.9),Controller ×0.95 |
业务层 330 → 359(×1.09) |
| 一句话结论 | 逻辑放进 Model,它的变更就波及不到上面两层 | 环境交出去,它的更换就波及不到业务层 |
| 最主要的手法 | 让 X 自己干(多态)· 口子先开好 | 把无关的交出去 · 每次只动一层 |
| 失败的样子 | 逻辑散到 Controller 里,每个需求惊动所有人 | 业务和环境缠在一起,换个数据库要重写业务 |
┌────────────────────────────────────────────────────────────────┐ │ ★ 两个问题的答案是同一件事:把业务从它所处的环境里剥出来。 │ │ │ │ 环境是"屏幕"(26~28)、是"网络"(29~30)、 │ │ 是"某个 RPC 框架"(41)、是"某个数据库"(42)、 │ │ 是"某套帐号体系"(43~44)—— 一样的。 │ └────────────────────────────────────────────────────────────────┘★ 而这也是整个"架构"这个词在这九讲里的具体含义:
架构不是画类图,是决定哪些东西必须能被换掉,然后把它们放到能被换掉的位置上。45 讲会把这件事正式讲一遍(“怎么做详细设计”),
而 60 讲那句"架构分解:边界,不断重新审视边界"说的就是这九讲一直在做的动作。
八 · 一张时间线(qpaint 全程)
v26 109 行 dom.js,能画图
v27 加选择工具 Model +197
v28 离线持久化、对象 ID Model +191
v29 ★ 第一次有服务端(mock,657 行 Go) 浏览器端零改动
v30 两边真的接起来 Model +359
v31 控件化(辅助界面元素)
────────────────────────── 桌面开发篇结束,服务端开发篇开始 ──────────────────────────
v41 ★ 换 RPC 框架 + 重写测试 协议层 286→169,业务层零改动,第一次有依赖
v42 ★ 换存储 + 多租户 + mock 授权 业务层几乎全重写,测试翻倍,多了一份设计文档
(43 讲:纯概念,零代码)
v44 ★ 空的(= v42)
v44-bear ★ 网关做认证 paintdom 零改动,网关 35→192
★ 最后看一眼这条线上最长的那两段"零改动":
v28→v29(加了一整个服务端,浏览器端零改动)
v42→v44-bear(加了一整套帐号授权,业务服务零改动)这两段零改动,就是这九讲全部的成绩单。
验收
| # | 问题 | 答案在 |
|---|---|---|
| 1 | 四讲下来业务层涨了多少行?这个数字说明什么? | 二 · 2.1 |
| 2 | 协议层为什么先变少(286→169)又变多(169→230)?加回来的和砍掉的性质有什么不同? | 2.2 |
| 3 | 42 讲测试翻倍,新增的是什么测试?它把什么变成了可执行的? | 2.3 |
| 4 | 后端四讲唯一的主线是什么?它怎么回答"服务端难在基础软件多"这个问题? | 三 · ① |
| 5 | 怎么用一次改造来检验分层? | 三 · ② |
| 6 | mock 三次(29 / 41 / 42 讲)分别 mock 了什么?共同的手法是什么? | 三 · ③ |
| 7 | "声明式约束 > 每次记得检查"在四讲里出现了哪三次? | 三 · ④ |
| 8 | "不能消除失败就选择失败的样子"在哪三处出现过? | 三 · ⑥ |
| 9 | 详细设计文档的四段是什么?为什么顺序不能反? | 五 |
| 10 | 诚实清单里最重要的两条代码事实是什么? | 6.1 |
| 11 | 前端五讲和后端四讲问的问题不同,答案为什么是同一件事? | 七 |
答案
1. 只涨了 29 行(drawing.go + shape.go:330 → 359),而这四讲干的是换 RPC 框架、加多租户、换数据库、加帐号授权。
说明两层分层是真的成立的。而它和前端五讲那组数字(dom.js ×7.9、Controller ×0.95)是同一件事的两面:业务的变化落在业务层,环境的变化落在环境层——一个分层是否成立,看的就是"变化有没有落在它该落的地方"。
2. 41 讲引入 restrpc 砍掉 117 行纯样板(取参数、解 body、序列化、翻错误码,同一件事重复 8 遍);42 讲加回 61 行真业务约束(自定义 Env 解析授权 + 每个方法多一个 env.UID——“每个请求必须知道是谁在调”)。
框架只能替你消掉第一种。 第二种(横切的业务约束)只能自己写,但框架给了它一个正确的落点(OpenEnv),于是只写一遍而不是八遍。
3. 新增 6 个序列化测试,形状都是:json.Marshal(val) 和 bsonMarshal(val)(存进去再读回来再按 API 格式输出)必须等于同一个写死的字符串。
它把 README_IMPL 里那两条人话约束变成了可执行的东西(“json.Marshal 符合 API 层预期” + “bson.Marshal 符合 mongodb 预期”)。
判断:一个约束如果只写在文档里,它就会在某次重构里悄悄失效;写成测试,它才真的存在。
4. 主线是把"跟业务无关的部分"一层层交出去:HTTP 解析/序列化→restrpc,测试样板→httptest/mockhttp,持久化/并发/唯一性→mongodb + 唯一索引,帐号授权→dex/JWT/网关。剩下的 359 行才是 QPaint。
它这样回答那个问题:解法不是"学会自己写这些基础软件",是"学会认出哪些不该自己写"——而"认出"恰恰需要知识面和理解深度,因为你必须懂它,才知道该不该交出去、以及交出去之后代价落在哪。
5. 改造之前先预测"应该改哪些文件",改完对一遍。
四讲的实测:41 讲只动 service.go + 测试;42 讲主要动 drawing.go,协议层只加一个 uid;44 讲只动 paintweb,paintdom 全部字节相同。
对不上,就是分层的位置有问题——而这比改动本身更值得关注。
6. 29 讲 mock 了整个服务端(全内存,进程一停全没);41 讲 mock 了 restrpc.Env(空壳,一点没用);42 讲 mock 了授权(浏览器自己声称 QPaintStub 1)。
共同手法:先把"这件事发生在哪一层、结果长什么样"钉死,实现给最笨的那个。接缝定对了,换实现就是局部替换。
最好的样本是 42 讲那个假授权:它"任何人都能伪造",看起来毫无价值——但它定下的接缝(env.UID)原封不动活到了最后,于是真授权来时业务服务连重新编译都不需要。
7. ① 唯一索引取代 if 检查 + mutex(42 讲);② 把 uid 写进查询条件取代"查出来再判 403"(42 讲);③ 网关删掉 Cookie 取代"业务服务自己也检查一下"(44 讲)。
通用手法:把"要记得做的事"变成"不做就编不过 / 查不到 / 看不到"——if 只保护写了它的那一条路径,声明式约束保护所有路径,包括你还没写的那些。
8. ① 42 讲的多步修改宕机:先建被指的、后建指针;先删指针、后删被指的,让残留物是"看不见的垃圾"而不是"看得见的错误";② 43 讲的无状态 token 不能吊销:让它短命;③ 39 讲的缓存与存储不可能原子:接受不一致,用 TTL 把"永久错"降成"错 5 分钟"。
三条是同一条:工程里很多问题没有"解决",只有"降级到可接受的形状"。而"选哪个形状"往往是免费的(换个顺序、改个数字),比真解决便宜几个数量级。
9. ① 逻辑 DOM 结构(有什么)② 使用界面(能被怎么用,含语言之外的约束)③ 数据结构(表、字段、索引)④ 实现逻辑/算法(用一种可执行的语言写伪代码)。
顺序不能反,因为每一步给下一步定题:①→② 接口只能围绕这些实体写;②→③ 表和索引由"要跑哪些查询"决定;③→④ 算法只能用存储支持的操作拼出来。
反过来做(先选数据库再想接口)会得到"接口长得像表的 CRUD、业务语义全在调用方"的系统。
10. ① 原文 44 讲让你看的 compare/v42...v44 是空的(0 commit,v44 = v42)——接 dex 的代码不存在;真正有代码的是 v44-bear。② 42 讲把 SetZorder 弄丢了(换存储后变成 return errNotImpl),而原文一个字没提。
(还有三条同类:$push 不幂等、没用事务而是靠顺序、v44-bear 里三处留白。)
这些都不是"写错了",是 DEMO 的合理取舍——但它们说明:读实战代码必须去看真实的 diff,正文说的和代码做的不总是同一件事。
11. 因为两个答案都是把业务从它所处的环境里剥出来。前端问"什么该放进 Model"(需求在长),后端问"什么不该自己写"(环境在换),而环境是"屏幕"、是"网络"、是"某个 RPC 框架"、是"某个数据库"、是"某套帐号体系"——一样的。
★ 这也是"架构"这个词在这九讲里的具体含义:架构不是画类图,是决定哪些东西必须能被换掉,然后把它们放到能被换掉的位置上。
下一步
45 讲:架构,怎么做详细设计?——42 讲那份 README_IMPL.md 的方法论版本。
先读它之前,建议再看一眼那份文档(v42-服务端/README_IMPL.md),
因为 45 讲讲的就是它那四段。
四讲的带读:
带读 15 · 41 讲(RPC 框架 · 分层的可观测指标 · 测试 DSL)
带读 16 · 42 讲(最厚的一讲:多租户 · 存储 · 索引 · 顺序与事务)
带读 17 · 43 讲(OAuth 的零点 · 双 token · 授权码模式)
带读 18 · 44 讲(网关翻译授权 · JWT · 不造轮子的四步)
前端五讲的收尾(对照着看):
带读 08 · 26~30 讲收尾(Model 层的厚度 · 六个手法 · 16 条判断)
