团队代码评审会上,架构师往群里扔了三张截图:同一张订单表,A组写的是getOrderInfo,B组写的是queryOrderById,C组写的是fetchOrderDetail。三个人都觉得自己没写错,会议室陷入熟悉的尴尬沉默。这种场景我在五六个项目里都见过,每一次的结局都一样——先吵十分钟命名,然后妥协成"下次统一",然后下个迭代继续吵。直到我开始牵头整理t3code,才算是真正把这类问题按住了。
t3code 不是什么新框架,也不是什么脚手架工具,它本质上是一套从实际项目里长出来的编码共识体系——把"类型怎么设计、模块怎么分层、调用链怎么追踪、命名怎么统一"这些平时靠口头约定、靠 Code Review 靠运气的事情,变成一套可以照着抄、可以自动检查、新人进来三天就能上手的规则集。这篇文章我把这套东西完整拆开讲,包括它的三层设计逻辑、分层模型的具体边界、落地时踩过的坑,以及最后它给团队带来的量化收益。如果你也在为一个"代码越写越乱、各写各的"的团队头疼,这篇应该能给你一些直接能用的思路。
1. t3code 这个代号,本质上是一套编码共识而不是代码框架
先说清楚 t3code 到底是什么。这个代号里的 t3,在我这边的含义是 Type、Trace、Ternary——类型优先、调用可追踪、三方约定。听起来有点玄,展开说就是三件事:所有业务数据先用类型定义清楚,所有核心调用链必须能从日志里反查完整路径,所有跨模块协作规则必须是三方以上确认过的共识。这个定位从一开始就和那些"又一套最佳实践文章"划清了界限:它不教你写某种特定代码,它解决的是团队里"每个人都在写自己风格的代码"这件事。
1.1 为什么叫 t3 而不是 v3 或者别的名字
早期内部文档里,这个东西叫过code-style-v3,也喊过shared-lint,后来都否了。v3的问题是它暗示了"第三版规范",实际上我们并没有一个正儿八经的 v1 和 v2——之前的那些约定散落在十几个文档和几百条群聊记录里,根本不算版本。shared-lint的问题更明显,它只覆盖了 Lint 层,而 Lint 永远检查不出"这个服务该不该存在""这里应该是领域服务还是应用服务"这种架构级问题。
最终定名 t3code,是因为它确实有三个互相咬合的核心支柱。第一根柱子叫 Type,一切从类型出发,后端接口先定 Schema,前端组件先定 Props,跨端事件先定事件负载。第二根柱子叫 Trace,任何一次用户请求,从 HTTP 入口到数据库查询,必须能在日志系统里还原出完整链路,做不到这一点的代码一律视为不合规。第三根柱子是 Ternary,它的意思是"三方约定",任何跨模块的规则不是某个人拍脑袋定的,而是消费方、提供方、维护方至少三方对齐后的结论。
这三根柱子对应了我在团队里观察到的三类最常见混乱:类型不统一导致联调时互相猜字段,调用链不透明导致线上问题定位像大海捞针,约定不一致导致 A 组写的代码 B 组维护时想骂人。t3code 就是冲着这三类问题去的。
1.2 t3code 的适用边界,别把它当成银弹
这里要泼一盆冷水。t3code 不是万能药,它在两种场景下效果最好,在其他场景下你可能需要裁剪。第一种场景是 10 到 50 人的中型研发团队,业务复杂度上来了,但还没有大到每个小组完全独立成微型公司。这种规模最尴尬,人少的时候靠默契,人多的时候靠治理架构,中间这一段靠的就是一套共识体系。第二种场景是服务化改造中期,系统正在从单体往服务拆分的路上,模块边界每天都在变,如果没有一套约束,拆着拆着就会变成"伪微服务"。
反过来,三五个人的创业项目、一个几十年的遗留单体、或者一个纯粹内部工具类项目,t3code 的价值会打折扣。不是说完全没用,而是付出的维护成本可能超过收益。我们当时也花了两个多月把存量项目的核心模块逐步对齐到 t3code 的规则上,这个过程并不轻松。所以我的建议是:新项目从第一天就按 t3code 走,老项目挑核心链路逐步迁移,不要搞一刀切。
在具体展开规则之前,还有必要说明一点:t3code 的所有规则都不是拍脑袋定的。每一类规则后面都有至少一个真实事故支撑。比如命名之所以要统一,是因为我们真有一次线上故障,排查时发现getUserList和queryUserPage其实是同一个接口,两个团队各维护一份,参数还不完全一样,联调时谁都没发现。后面你会看到,每条规则的来历我都尽量交代清楚,这样你在自己团队里推行时,也能讲得出"为什么"。
2. 四层调用模型:让模块边界从口头约定变成强制约束
t3code 的核心骨架是一个四层调用模型:presentation(表现层)、application(应用层)、domain(领域层)、infrastructure(基础设施层)。很多人看到这个会第一时间想到整洁架构、六边形架构,没错,思路是相通的,但 t3code 做了两个非常具体的收紧。
第一个收紧是依赖方向被写死成单向的:表现层可以依赖应用层,应用层可以依赖领域层,领域层谁也不依赖,基础设施层被下面的领域层通过接口反向依赖。任何反向依赖在 Code Review 里一票否决。第二个收紧是每一层都有自己专属的命名后缀和目录结构,你在 IDE 里扫一眼文件路径就能判断一个文件属于哪一层,不需要打开文件看内容。
2.1 每一层的职责边界和命名后缀
四层各自管什么,我用一个用户注册的例子来拆。表现层只做三件事:接收输入、调用应用层、格式化输出。在 NestJS 项目里就是 Controller,在 React 项目里就是页面组件和 hooks,它们的命名后缀是Controller、Page、Hook。应用层是编排者,它负责把一个业务用例的步骤串起来——查重、创建用户、发欢迎邮件、记录审计日志,这些步骤谁先谁后由应用层决定,后缀是UseCase或Service里的业务服务。领域层是业务规则的家,像"邮箱格式必须合法""用户名不能重复"这种规则只能住在这一层,后缀是Entity、ValueObject、DomainService。基础设施层是技术细节的藏身处,数据库查询、外部 API 调用、消息队列发送,全在这里,后缀是Repository、Client、Mapper。
为了让你直观感受这个划分,我贴一个简单的用户注册用例代码,注意看依赖方向:
// domain 层:不依赖任何框架,定义业务规则 @Entity export class User { private constructor( public readonly id: UserId, public readonly email: Email, public readonly createdAt: Date ) {} static create(input: { email: string }): User { const validEmail = Email.create(input.email); // 业务规则:邮箱合法性 if (!validEmail.isValid()) { throw new InvalidEmailError(input.email); } return new User(UserId.generate(), validEmail, new Date()); } } // domain 层:仓储接口,具体实现交给 infra 层 export interface UserRepository { findByEmail(email: Email): Promise<User | null>; save(user: User): Promise<void>; } // application 层:编排用例,不写具体业务规则 export class RegisterUserUseCase { constructor( private readonly userRepository: UserRepository, private readonly welcomeMailer: WelcomeMailer ) {} async execute(input: RegisterUserInput): Promise<RegisterUserOutput> { const user = User.create({ email: input.email }); // 规则在领域层判断 const existing = await this.userRepository.findByEmail(user.email); if (existing) { throw new DuplicateEmailError(user.email); // 异常也分领域层异常和应用层异常 } await this.userRepository.save(user); await this.welcomeMailer.send(user.email); return { id: user.id.value }; } } // infrastructure 层:实现仓储接口,写具体的 SQL 或 ORM 操作 @Injectable() export class TypeOrmUserRepository implements UserRepository { async findByEmail(email: Email): Promise<User | null> { const row = await this.dataSource .getRepository(UserEntity) .findOne({ where: { email: email.value } }); return row ? UserMapper.toDomain(row) : null; } }这段代码里有几个细节值得注意。第一,User.create是领域层的静态工厂方法,邮箱合法性校验在领域层,应用层完全没有触碰规则判断。第二,DuplicateEmailError是领域层异常,它的语义是"业务上的重复",而不是"数据库唯一索引冲突",所以应用层在捕获时可以直接映射成 HTTP 409。第三,UserRepository接口定义在领域层,但实现在基础设施层,依赖方向完全反转,这样替换数据库实现时不会波及上层。
2.2 依赖方向检查清单:哪些写法一票否决
把四层模型落地到评审里,我总结了一张一票否决清单,命中的代码直接打回,不用讨论:
- 表现层的 Controller 里直接调用
userRepository.findByEmail,跳过应用层——不允许,表现层只认识应用层的 UseCase。 - 领域层的 Entity 里出现
TypeOrm的装饰器或者axios请求——不允许,领域层不 import 任何框架和基础设施。 - 基础设施层的 Repository 返回 ORM 实体而不是领域实体——不允许,Repository 的接口签名必须使用领域对象,Mapper 负责转换。
- 某个 Service 类把表现层、应用层、领域层的逻辑全写了——不允许,一个类只能属于一层,通过后缀和目录判断。
- 反向依赖,比如应用层 import 了表现层的东西——直接打回,这是依赖方向的根本性破坏。
这套清单的价值在于它把"架构评审"这个原本依赖资深工程师经验的事情,变成了机械检查。任何一个人对照清单过一遍代码,都能给出初步结论。我们后来甚至把这套清单的一部分写进了 ESLint 自定义规则,自动拦截一部分违规。
2.3 为什么必须处理跨层异常的类型
分层之后最常见的并发症是异常处理混乱。很多团队分层是分了,但异常还是从底层一路throw到顶层,中间谁也不知道这个异常是什么意思,最后 Controller 统一返回 500。t3code 对异常有一套自己的规约,核心是"异常必须和业务语义绑定"。
我习惯把异常分成三类:领域异常、应用异常、基础设施异常。领域异常表达的是业务规则被违反,比如DuplicateEmailError、InsufficientBalanceError,这类异常在应用层被捕获后可以直接映射为 409 或 422 之类的 HTTP 状态码。应用异常表达的是用例执行过程中的流程性错误,比如UserNotFoundInContextError,通常映射为 404。基础设施异常表达的是外部依赖出了问题,数据库连不上、第三方 API 超时,这类异常要包一层再往外抛,比如DatabaseConnectionError,并且必须带上上下文信息,方便追踪。
异常这件事,踩过的坑非常典型。我们有个服务,线上经常报 500,日志里只有一行TimeoutError,你根本不知道是 Redis 超时、MySQL 超时还是外部接口超时。后来把异常按上述三类重新规整,给基础设施异常加了source字段,问题一发生就能从日志里直接看到"MySQL 连接池获取超时"还是"调用支付网关超时",定位速度快了不止一倍。
3. 从命名到调用链:t3code 的落地细节与自动化检查
分层模型是骨架,下面要讲的这些细节是血肉。很多团队的分层模型长得差不多,但真正产生差异的正是在命名、目录结构、调用链追踪这些日常细节上。这一部分我尽量给到可以直接抄作业的程度。
3.1 命名规范:为什么统一命名不是审美问题
很多工程师觉得命名是审美问题,但实际上它直接影响排错效率和跨团队协作成本。t3code 的命名规约有三条最核心的,每一条都对应一个真实事故。
第一条是"动词开头,完整语义,禁止缩写歧义"。同样一个查询方法,getUserInfo、getUserProfile、getUserDetail看着差不多,实际可能指向三个不同接口。t3code 要求方法名动词开头,并且后面的名词必须是领域模型的全名,getUserByEmail、listOrdersByUserId,不允许出现info、detail、data这种语义模糊的词。哪怕方法内部实现一模一样,名字不同也要统一,因为名字是代码的公共契约。
第二条是"布尔属性一律用 is/has/can 前缀"。flag、status这种命名一律禁止,特别是status字段,它到底是枚举还是布尔,光看名字根本判断不了。统一用isActive、hasPermission、canRetry之后,条件表达式的可读性会明显提升,而且 ESLint 规则可以直接检查。
第三条是"类名后缀即层名"。我之前提到过,Controller、UseCase、Entity、Repository、Client 这些后缀不是装饰,它们让任何人扫一眼命名就能知道代码属于哪一层。这也让架构检查变成了字符串匹配,自动化成本极低。
下面这张表是我们内部贴在每个项目 README 里的命名速查表:
| 层 | 文件名后缀 | 典型类名 | 禁止事项 |
|---|---|---|---|
| 表现层 | .controller.ts/.page.tsx/.hook.ts | OrderController、CartPage | 后缀不能是 service/repository |
| 应用层 | .usecase.ts/.service.ts | CheckoutUseCase | 不能直接操作数据库 |
| 领域层 | .entity.ts/.vo.ts/.domain-service.ts | OrderEntity、Money | 不能 import 框架代码 |
| 基础设施层 | .repository.ts/.client.ts/.mapper.ts | TypeOrmOrderRepository | 不能定义业务规则 |
3.2 调用链追踪:Trace 支柱落地的具体做法
Trace 支柱在 t3code 里对应的是一套"链路追踪字段规范"。核心就一句话:每一次外部请求、每一次跨服务调用、每一次关键数据库写操作,必须携带一个统一的traceId,并且这个 traceId 要从 HTTP 入口一直传递到数据库操作层。
具体落地上,我们在日志格式层面做了强制要求。每个服务在打印日志时,必须包含三个字段:traceId(全链路追踪 ID)、spanId(当前调用单元 ID)、serviceName(当前服务名)。以 Node.js 生态为例,我们用pino作为日志库,配合cls-hooked或者 AsyncLocalStorage 在请求上下文里维护 traceId,任何一层打印日志都自动带上这份信息,不需要每个开发手动传参。
// 中间件:HTTP 入口生成或透传 traceId import { randomUUID } from 'crypto'; import { AsyncLocalStorage } from 'async_hooks'; export const asyncLocalStorage = new AsyncLocalStorage<{ traceId: string }>(); export function traceMiddleware(req, res, next) { const traceId = req.headers['x-trace-id'] ?? randomUUID(); res.setHeader('x-trace-id', traceId); asyncLocalStorage.run({ traceId }, () => next()); } // 任意业务代码中打印日志,无需手动传 traceId export function logInfo(message: string, meta: Record<string, unknown> = {}) { const context = asyncLocalStorage.getStore(); logger.info({ message, traceId: context?.traceId ?? 'unknown', serviceName: 'order-service', ...meta, }); }这套东西的价值在线上故障排查时体现得最明显。以前查一个问题,要在日志平台里像大海捞针一样搜用户 ID,然后手工把几十条日志串起来猜调用顺序。现在直接拿 traceId 搜一遍,从网关入口到 SQL 慢查询,整条链路的时间消耗一目了然。有一次我们定位一个订单超时问题,就是因为 traceId 串联出了"应用层调支付网关时,卡在了一个重试循环里"这个事实,而这在分散日志下几乎不可能快速发现。
为了把 Trace 支柱从"看日志习惯"变成"强制约束",我们还加了几条硬性验收标准。第一,任何对外提供的 HTTP 接口必须接受x-trace-id请求头,并且回写响应头,不允许丢弃上游 traceId。第二,业务日志中不得使用console.log,统一走封装好的 Logger,否则 traceId 大概率丢。第三,跨服务调用时,traceId 必须通过请求头透传,内部 SDK 统一封装了透传逻辑。
3.3 目录结构模板:照着建就能少吵十次架
分层模型和命名规范最终要落到目录结构上,否则每个人建目录时还是自由发挥。t3code 给出了一套标准模板,新项目直接按这个建,老项目逐步对齐。以我们最常用的 NestJS 项目为例,目录长这样:
src/ ├── presentation/ │ ├── http/ │ │ ├── controllers/ │ │ └── dto/ │ └── consumers/ # 消息队列消费者也属于表现层入口 ├── application/ │ ├── usecases/ │ └── ports/ # 定义应用层需要的接口 ├── domain/ │ ├── entities/ │ ├── value-objects/ │ └── domain-services/ ├── infrastructure/ │ ├── persistence/ │ │ ├── repositories/ │ │ └── mappers/ │ ├── http-clients/ │ └── messaging/ └── shared/ # 跨层共享的常量、工具,注意不是垃圾桶这个模板有一个很容易被误解的地方:shared目录。很多人把它当成放杂物的抽屉,什么都往里扔,最终 shared 变成了一个依赖黑洞,各层都依赖它,它自己又依赖了各层,依赖方向被彻底破坏。t3code 对 shared 的定位非常严格——它只能放纯函数、常量、基础类型,不允许放业务逻辑,不允许 import 任何业务模块,并且它的体积应该非常小。出现"shared 越来越大"这个信号时,一般说明分层已经出了问题,该拆的业务逻辑没有拆到合适的层里去。
3.4 自动化检查:把规则写进 CI,而不是停留在文档里
任何规则,如果只靠人来执行,最后的结局一定是逐渐腐烂。t3code 在自动化方面做了三件比较实在的事。
第一,ESLint 自定义规则。我们基于@typescript-eslint/parser写了十几条自定义规则,比如"禁止 import 跨层模块""禁止非 Controller 文件使用 @Controller 装饰器""类名后缀必须匹配目录层级"等。这些规则和普通代码规范一起跑在 CI 上,违规直接 build failure。写自定义 ESLint 规则的成本没有想象中那么高,读一下 AST 的结构,写个createRule,半天就能上手。
第二,目录结构约束脚本。我们写了一个简单的 Node 脚本,扫描src下的目录,校验是否符合模板结构,并且检查文件后缀和所在层级是否匹配。这个脚本大约 200 行,挂在 CI 里跑一遍不到一秒,但它拦截了很多低级错误。
第三,依赖方向可视化。在大型项目里,人和 CI 都很难看出依赖关系是否被悄悄破坏,我们引入了一个现成的工具来生成模块依赖图,每两个星期跑一次,提交到代码评审群里。一旦发现 infrastructure 层被 domain 层反向依赖,或者出现了循环依赖,就会被立刻揪出来。这件事在服务拆分阶段特别有价值,因为拆服务时的核心任务就是搞清楚模块边界,依赖图直接把这个边界画在了眼前。
4. 推行 t3code 的实操顺序与踩坑复盘
这一部分是整篇文章里最"带血"的部分。t3code 从文档变成团队实际遵守的规则,中间经历了差不多三个月的反复,有些规则被抵制过,有些规则我自己后来都推翻了。把这些过程写出来,是希望你不要重复走这些弯路。
4.1 推行顺序:先立样板工程,再铺开存量项目
第一版 t3code 文档写完之后,我们犯过一个方向性错误——直接开全员宣讲会,把所有规则一次性倒给所有人,结果大部分人的反应就三个字:"记不住"。90 页的文档,正常人根本没有动力去读。
后来我们调整了策略,把推行顺序分成三步。第一步是"立样板",找一个新启动的、业务复杂度适中的项目,全程按 t3code 的规则开发,把它当成标杆。这个项目不要求大,但要能覆盖四层模型的每一种形态。第二步是"结对渗透",安排标杆项目的核心开发者作为 t3code 的布道者,分散到各个业务组,在日常开发中手把手带。这一步比任何文档都有效,因为规则在具体场景里的取舍,只有做过的人才能讲清楚。第三步是"存量迁移",挑核心链路的高频代码模块,逐步对齐新规范,非核心模块暂时不强求。
这个方法的核心逻辑是:规则这种东西,光靠宣讲是进不了肌肉记忆的,必须靠实际项目反复练习。
4.2 踩坑一:表现层直接调基础设施层,规则被撕开口子
这是推行过程中我们遇到的最大阻力。很多业务开发者习惯了在 Controller 里直接用 ORM 的 Repository,因为这样写最快,少写好几层代码。t3code 强制要求经过应用层,在他们看来是"多此一举"。
当时组里有位资深工程师直接在评审里往群里丢了一段代码,声称"我这样写完全没问题",而那段代码恰好就是跳过应用层直接查库的典型。我没有正面争论,而是找了一个线上故障作为案例:某个 Controller 直连数据表时,由于没有经过应用层的权限校验,把"是否已删除"的过滤逻辑漏了,导致一条已删除的数据被展示给了所有用户。这个事故在旧代码里真实发生过,用它来论证"绕过应用层"的危害,比讲一百遍架构原则都管用。
规矩最后是这样定下的:Controller 可以调用 UseCase,但 UseCase 内部可以组合多个领域服务和仓储接口,完全不允许 Controller 直接触碰 Repository。为了落地这个规则,ESLint 规则里直接限制了 Controller 文件只能 import 应用层的 UseCase 和 DTO,其他 import 一律报错。
4.3 踩坑二:领域层必须零依赖,这条规则一开始被所有人反对
领域层不依赖任何框架,这件事听着高级,做起来很难受。最先吐槽的是写数据库的同事:用 TypeORM 的话,实体上不带@Entity装饰器,Repository 就没法识别,你怎么映射?这个问题迫使我们引入了两次映射:数据库实体(带装饰器的 ORM 模型)和领域实体(干净的纯业务对象),中间通过 Mapper 进行转换。
如果你用过 TypeORM 的Entity, 你会发现从 ORM 实体映射回领域实体这层代码很琐碎,而且容易遗漏关联关系。我们当时的解决方案很朴素:写一个统一的 Mapper 工具函数,字段名一一对应,跑完善的单元测试。代价是每次加字段要多写两行映射代码,但收获是领域层彻底与技术框架解耦,未来换 ORM 或者换数据库厂商时,业务规则层完全不需要动。
这里我想多说一句:领域层零依赖这条规则真的是"痛并值得"。它最大的收益不是未来能换框架,而是领域层的实体彻底变成纯粹的、可单独单测的业务对象。我们的OrderEntity可以在完全没有数据库的情况下进行单元测试,测试速度从秒级降到毫秒级,CI 时间明显缩短。
4.4 踩坑三:规则被机械套用,新代码出现结构僵化
推行到第二个月时出现了一个有意思的现象:大家确实按规则写了,但写出来的代码看起来很奇怪——到处都是"为了分层而分层"。比如一个特别简单的"根据 ID 查询用户昵称"的功能,也被拆成 Controller + UseCase + Repository + Mapper 四层,代码行数是以前的几倍,可读性反而下降。
这说明规则在执行过程中被教条化了。t3code 并不是要求所有代码都过四层,它要求的是"业务规则必须有归属,依赖方向必须正确"。对于最外层查询这类没有业务规则的简单透传场景,我后来在文档里加了一条"简化例外":如果某个用例不包含任何业务规则,仅仅是数据透传,允许 Controller 直接调用领域层的查询接口,但禁止直接操作数据库。这个例外加完,模板化和过度设计的问题缓解了,规则也变得更贴合实际。
4.5 踩坑四:存量项目的迁移不能靠一次性重构
老项目要全量对齐 t3code,一开始我们天真地想搞一个"重构月",停掉需求,把存量代码集中三个月改完。结果需求方直接炸了,业务根本不给你三个月空窗期。
后来换成"增量对齐+核心链路优先"的策略:每次开发新需求时,把涉及到的老代码顺手按新规范整理,不做大范围重写,重点保证核心交易链路和用户主链路符合 t3code。这样做了四个多月,存量核心链路的覆盖率达到了 80% 以上,而业务几乎感受不到重构的存在。如果你也要迁移老项目,我强烈建议采用这个"随行就市"的思路,而不是专门立项重构。
5. t3code 的量化收益与我的个人体会
说了这么多规则和踩坑,最后还是落到收益上,不然这套东西就只是理论自嗨。我们没有用什么复杂的方法论去评估,就用几个最朴素的指标做了前后对比。
5.1 三个最直观的变化
第一个变化是新人上手速度。以前一个刚入职的工程师,看懂一个订单模块的代码至少两到三周,中间要反复问"这个 Service 是干嘛的""这条路为什么这么走"之类的问题。有了 t3code 之后,新人第一周先读分层模型说明,第二周就能在带教下提交符合规范的需求代码。因为我们目录结构是标准的,看到路径就能猜到大概逻辑,看到文件名后缀就知道它属于哪一层,学习成本被大幅压缩。
第二个变化是跨团队联调的返工次数。以前前后端联调,最怕的是一来一回接口字段对不上,前端说userId后端说uid,两个人查半天发现是同一个东西。t3code 推进了接口字段的 Schema 先定义流程后,这类因命名不一致引发的联调返工基本消失。我们的记录显示,跨团队联调的平均返工次数从 4.2 次降到了 1.1 次。
第三个变化是线上问题定位时间。这一点前面在 Trace 支柱里提过,这里给个具体数字。引入统一 traceId 之前,一个典型的中等复杂度故障,从报警到定位根因平均要 35 分钟;之后这个数字降到了 12 分钟以内。对于交易类系统来说,这 20 多分钟的价值,你懂的。
5.2 一些写在最后的体会
如果你问我 t3code 最核心的经验是什么,我会说:规范类的东西,最难的不是制定,而是让所有人相信"照着做真的有用"。所以每一规则都应该有一个"因为某个事故所以存在"的来由,而不是"我觉得这样更好"。我在团队里反复讲的一个比喻是:编码规范就像交通规则,红灯停绿灯行本身不产生价值,它产生价值的方式是所有人都遵守它,从而让整个交通系统变得可预测。单独一个人闯红灯可能更快,但大家都闯的时候,谁也快不了。
最后分享一个我在推行过程中养成的习惯:每次评审打回代码时,除了指出"不合规",还要顺手写上"为什么不合规"以及"建议怎么改"。一开始很费时间,但这样重复一个月后,合规率就会显著上升,因为人都需要知道规则背后的理由,才愿意真正遵守它。把这条也作为你推行 t3code 的第一步吧。