我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

T3Code:从能跑到能演进的代码质量三级标准

T3Code:从能跑到能演进的代码质量三级标准 接手过别人烂代码的人大概都能理解那种感觉一个函数五百行变量名是data1、data2、temp改一个bug要顺着调用链摸半天最后发现坑在某个不起眼的全局状态里。我有一段时间就是在这种项目里挣扎后来痛定思痛慢慢整理出了一套自己的代码质量体系给它取名叫t3code。这东西不是什么惊天动地的框架也不是某门语言的特定规范而是一套从能跑到能看懂再到能演进的三级标准覆盖命名、结构、设计、优化、协作几个层面。这几个月我试下来效果比我预想的要好不少于是决定把这套方法完整地写出来给那些正在被代码质量困扰的团队和个人做个参考。这套东西不挑语言、不挑项目规模哪怕是个人开源项目或者一两个人的小产品也能直接用。1. 从能跑到能演进T3Code到底是什么1.1 我为什么开始整理这套标准一开始真的不是什么高深理由就是受不了了。团队里代码review的时候经常出现这段代码逻辑没问题但过一个月我们自己都看不懂的尴尬局面。线上出bug排查效率完全取决于当初写这段代码的人此刻在不在线。文档有但文档跟代码是两套叙事维护文档本身又成了额外负担。后来有一次一个同事请假两周我接手他一个模块光是搞明白那个状态机的流转逻辑就花了大半天最后发现设计文档里描述的流程和实际代码对不上。那一刻我意识到问题不是某个人的能力而是缺少一套所有人心照不宣的代码组织方式。于是我开始观察那些开源界公认写得漂亮的项目再对照我们自己的代码逐条总结差异慢慢形成了t3code的雏形。定义上t3code把一段代码从写出来到被维护的整个过程分成三个等级T1、T2、T3每一级都有明确的核心关注点。这三级不是彼此独立的更像是层层递进的台阶你不可能在函数命名一塌糊涂的情况下去谈什么架构演进同样代码能跑了也只是最低标准离可维护、可复用还差着两层楼。1.2 T3Code的三级结构命名规范、设计手法、演进能力我先直接给出框架图再一个一个解释T1清晰层目标是让人和机器都能快速读懂。重点在命名、格式化、函数边界、单一职责。这一层是地基地基不打牢上面全是空中楼阁。T2复用层目标是让代码可以被安全的复用和组合。重点在接口设计、依赖管理、模块划分、设计模式的应用。T3演进层目标是让系统能够低成本地应对需求变化和规模增长。重点在观测性、配置化、重构策略、领域边界。需要注意这套分级不是岗位职级不是说架构师才需要T3初级工程师只需要T1。实际上一个刚入职的新人写出的工具函数理论上也可以直接达到T2甚至T3的水准因为它衡量的永远是一个具体的代码片段或模块而不是某个人。1.3 T3Code不是银弹它解决什么、不解决什么很多团队一听说代码规范第一反应是我们也有规范但没用。这很正常因为大多数规范只停留在T1层面——列了一堆命名规则、缩进规则然后review的时候对着规则表检查。但是对于为什么这个函数要拆成两个为什么这个模块依赖反了这类更深层的问题基本没人管。t3code想解决的问题主要是这几类代码可读性差新人上手慢模块之间耦合严重改一处崩一片单元测试不好写因为逻辑和副作用纠缠在一起需求变更时小改动变成大工程知识只存在于老员工脑子里人一走知识就没了它不解决的问题也很明确它不帮你做技术选型、不规定你用哪种设计模式、不是性能优化指南、也不是项目管理方法论。它是一套关于代码形态的组织原则和具体技术栈无关。打个比方如果说编程语言是砖块设计模式是户型图那么t3code就是一套施工纪律——它保证每一堵墙都砌得直每一根梁都放得正。你不按纪律施工房子也能盖起来只是盖不高罢了。2. 第一级T1让代码先能被看懂2.1 命名规范变量、函数、文件的三层约定我见过太多代码问题不是写法错误而是表达失败。变量叫flag谁知道你代表什么flag函数叫handleData你处理了多少种数据文件叫utils.js里面装了二十个毫无关联的函数——这种文件我后来统一叫垃圾收纳箱。t3code对于命名的要求核心就三个准确、完整、领域化。准确isUserLoggedIn比checkUser准确getPendingOrderList比getList准确。完整不要吝啬那几个字母idx不如currentIndextmp不如tempValue。代码读的次数是写的几十倍那点打字成本早就在后续读代码时赚回来了。领域化用业务术语命名。用户模块里orderStatus而不是s支付模块里refundDeadline而不是endTime。领域化命名让代码自带上下文读代码的人不需要再翻译一次。文件名同理。我见过common.js、helper.js、utils.js这种文件里面什么都放久而久之成了无人敢动的黑洞。t3code的建议是文件名要能让人猜出里面大概有什么比如formatOrderData.js、validateEmail.js。如果一个文件必须叫utils才能装下所有东西那大概率是拆分的粒度出了问题。2.2 格式化与静态检查先机器后人工命名是主观的但格式化不是。这也是t3code里我最强硬的一条凡是机器能自动检查的问题就别让人在review时浪费口水。具体操作上每个项目都应该配置好三样东西格式化工具Prettier / Black / gofmt 等静态检查工具ESLint / Ruff / golangci-lint 等提交前钩子husky / pre-commit 等这三样配合起来能拦住一大批低级问题。比如缩进不一致、分号缺失、未使用的变量、明显的类型问题、某种反模式。我以前团队里有个人特别喜欢把多个逻辑塞进一行每次review都要提后来直接在CI里强制跑检查没过的一律打回省了不知道多少口舌。顺便说一句静态检查工具的规则不要一开始就拉满。我见过有人直接开了一百多条规则然后代码里一片红最后大家干脆把检查关掉了。合理的做法是先开高标准的核心规则然后随着项目演进逐步增加且每一条规则的启用都要有明确理由。2.3 函数边界单一职责的落地判断单一职责Single Responsibility Principle可能是被误解最深的一个原则。很多人以为一个函数只做一件事就是短——于是他们疯狂拆函数最后每个函数只有三行但调用链深不见底。t3code对函数边界的判断方法不是看长度而是看三个问题这个函数能不能被一句话说清楚它做的事情这个函数是否需要多处修改才能应对一个需求变化这个函数的入参和出参是否都在同一个抽象层级举个例子一个函数叫processOrder里面前半段解析JSON、中半段查库存、后半段发邮件、再末尾记录日志你很难用一句话说清楚它做什么——这就是不合格。但如果拆成parseOrderPayload、checkInventory、sendOrderConfirmationEmail、writeOrderAuditLog四个函数每个函数你一眼就知道它干嘛这就是合格。这里有个技巧我用了很久很有用写函数的时候先写注释——用一句话说明这个函数要做什么。如果写不出来说明边界不清晰继续拆如果写出来了就把这句注释当成函数名注释能写多长函数名也能近似参考这个长度。我发现这个注释先行的技巧比任何设计原则都更容易落地。抽象层级也是一个被忽略的点。parseOrderPayload是数据处理层sendOrderConfirmationEmail是外部交互层这两个就不该出现在同一个函数体里。很多面条代码就是因为不同抽象层级的逻辑混在一起一会儿在解析字符串一会儿在操作UI一会儿又在调API读者被迫反复切换上下文极其消耗精力。3. 第二级T2让代码能被复用3.1 抽离通用逻辑的时机判断不要早抽不要死等复用是好事但过度追求复用会导致代码抽象过度反而让人看不懂。t3code里我坚持一个原则先写业务代码当同一个模式出现两次以上再考虑抽象。不是三次是两次。两次重复就是信号。事不过三那套延迟抽离的规则更适合架构层面的大规模抽象对于函数级别的公共逻辑第二次出现时就应该敏感起来。理由是第一次出现是事实第二次出现是趋势第三次出现就是灾难。比如你在A模块写了一个convertOrderToExportFormat函数很快在B模块也发现了类似的逻辑那就应该把公共的部分抽到一个共享层。抽的时候要保持一个红线被抽出的函数不允许知道自己被谁调用它只负责处理输入和返回输出不持有调用方任何信息。这样后续C、D模块才能安全地复用它。3.2 接口设计与依赖倒置很多代码复用失败问题出在上层直接依赖了具体实现。比如某个业务函数里直接new了一个MySQLOrderRepository然后读写数据库。这导致如果后面想换数据源、加缓存、或者做单元测试时用内存版实现就得改业务代码。t3code推荐的接口设计思路核心是面向接口编程而不是面向实现编程。做法非常朴素调用方依赖抽象接口、抽象类、函数签名具体实现在外部注入构造函数、依赖注入容器、甚至手动传参写成代码大概是这种感觉// 不推荐直接在业务逻辑里依赖具体实现 class OrderService { private repo new MySQLOrderRepository(); async getOrder(id: string) { return this.repo.findById(id); } } // 推荐依赖抽象外部注入 interface IOrderRepository { findById(id: string): PromiseOrder; } class OrderService { constructor(private repo: IOrderRepository) {} async getOrder(id: string) { return this.repo.findById(id); } }这个设计够简单但价值巨大。测试时你可以注入一个InMemoryOrderRepository切换数据库时只改装配层业务代码完全不动。依赖倒置原则听起来很高深但落地不过就是这个程度。3.3 模块划分与目录结构约定模块划分是T2阶段的另一个重点。很多项目的痛点不是函数写不好是整个代码组织方式是乱的。比如说按类型划分目录——controllers、models、services、utils——这种划分方式在小型项目中还行一旦业务复杂起来就会变成灾难每个模块里都有各自版本的相关逻辑改业务需求时你得同时改四五个目录下的文件。试试按业务领域Feature划分效果会完全不同src/ features/ auth/ components/ services/ types.ts utils.ts order/ components/ services/ types.ts utils.ts payment/ components/ services/ types.ts utils.ts shared/ http/ logger/ config/这种结构的好处是每个业务领域的代码内聚改动一个业务需求时你待的地方只有一个目录。而真正跨领域共享的公共逻辑才放进共享层。这个做法在DDD领域驱动设计里叫聚合的思想但t3code不规定你必须用DDD那套战术模式你只需把**围绕业务划模块而不是围绕技术划层**这个理念用起来就够了。模块之间的依赖关系也要定个规矩业务领域之间不能互相依赖共享逻辑只能放在shared里并且shared不能反向依赖任何业务模块。有了这条红线你就不会出现为了图省事直接在一个模块里import另一个模块的内部实现的情况——这种import一旦出现两三天后两个模块就彻底耦合了。4. 第三级T3让代码能被演进4.1 状态管理与调试友好从可观测性入手代码写出来是给人看的但更是给未来调bug的人看的。一个系统的可维护性很大程度上取决于当线上出问题时你能否快速定位到根因。t3code在T3层面最重视的就是可观测性。怎么落地不是非得搭一套分布式追踪系统对于大多数项目来说做好这几件事就够了统一的日志规范什么级别的操作打info什么打debug什么打error错误日志必须包含上下文订单ID、用户ID等。结构化日志不要打 user save success 这种要打{ event: user.save.success, userId: 123, elapsedMs: 45 }方便检索和聚合。关键路径埋点创建订单、支付回调、退款、任务队列消费这类关键节点必须有点位。异常链的穿透性不要吃掉堆栈不要catch了打个日志就完事能向上抛就向上抛让最顶层统一处理。这里有个反面案例我印象很深。之前排查一个线上bug日志显示Error: null pointer at UserService.java:150但没有任何上下文信息。我们得靠部署时间和日志时间推断是哪个用户触发的后来又拿时间戳反查Nginx访问日志才找到线索。从那以后我要求所有业务异常必须携带业务唯一标识订单号、traceId、userId排查成本直接降了一个量级。4.2 重构策略小步快走的节奏控制演进不是一次轰轰烈烈的重写而是无数个小重构的累积。t3code对这个阶段的建议非常明确永远不要在大型重构的同时加新功能。重构的每一步都应该是可以独立合入的、不改变外部行为的修改。实操上的节奏大概是发现坏味道比如一个函数过长、依赖混乱决定重构方式拆函数、改接口、下推/上提逻辑只做重构不做功能变更跑测试确认行为未变合入这个节奏看着慢实际上长期来看是最快的。因为每次变化都很小review的人容易通过出问题了也容易回滚。最怕的是有人憋了三个星期扔出一个几千行改动的大PR说是重构优化实际上没人能review得动出了问题也没法定位是哪一步引入的。我见过不少项目死在大重写上。真的重写的诱惑是巨大的因为旧代码太烂让人感觉推倒重来比较省力。但事实是你如果没有建立起新的规范重写的结果只会是第二坨烂代码只是烂得比较新而已。更好的策略是绞杀者模式——用新代码逐步替换旧模块每次替换一块替换完立刻收获一块干净的地盘。4.3 文档与注释写给别人也写给未来的自己我一直认为代码中最常见的注释错误不是没写注释而是写了描述是什么而不是为什么的注释。看这两行// 将订单状态设为已支付 order.status paid;这种注释就是废话。代码本身已经说明了order.status paid。真正该注释的是这种// 先设为paid再发通知防止通知回调时查到旧状态 order.status paid; await notifyUser(order.id);前者是噪音后者是信息。t3code对注释和文档的要求就两点注释解释为什么而不是解释什么文档描述行为约定而不是复述代码至于文档我不推荐那种事无巨细的架构设计文档——那种文档通常写完就过期。真正有效的文档是README 说明项目是什么、怎么跑起来、目录结构是怎么划分的每个模块的 README 说明这个模块负责什么业务、关键流程是什么代码中的注释负责解释那些不读代码就不知道的约定还有一点我要单独强调文档跟着代码走不要独立存在。如果文档在Wiki里、代码在Git里那文档必死无疑。把文档放进Git仓库和代码一起变更、一起reviewdoc就是代码的一部分而不是代码的影子。5. T3Code落地过程中的关键经验与常见反例5.1 最容易毁掉整个项目的坑过度设计写规范容易落到真实项目里就难难在所有规则都有适合的边界。t3code推广过程中最大的阻力不是没人遵守而是有人过度遵守——为了抽象而抽象为了设计模式而设计模式。举个例子一个只有两个字段的消息格式转换有些人因为看了某本书非要搞一个TransformerFactory、AbstractMessageConverter、MessageStrategy三件套。看起来架构清晰实际上任何接手的人都会一脸问号我一个JSON.parse就能解决的问题为什么要浏览五个文件过度设计的技术特征我总结下来就一条抽象层级超过实际复杂度一个数量级。解决它的办法是T2那节说的抽离通用逻辑的时机判断按重复次数来不要按想象力来。你觉得未来可能会复用和现在真的复用了是两回事。写成业务代码等信号出现再抽离也不迟。5.2 团队协作与Code Review的配合个人写代码是一回事团队协作是另一回事。t3code要落地光有人写规范没用review环节必须跟上。我们的review流程经历了三个演进阶段阶段一review只看有没有bug不注重代码是否可持续维护。阶段二开始把可维护性作为review要点但review全凭个人经验没有统一标准经常引入争论。阶段三以t3code的三级标准为checklistreview从主观评价变成按标准检查。阶段三视角下reviewer对提交的代码会按T1、T2、T3三个维度问问题T1命名够不够准确函数边界清不清楚静态检查有没有过T2这里是否重复出现两次该不该抽公共层依赖方向对不对T3这个模块将来需求变化时改动范围会不会失控日志上下文够不够定位问题有了这套checklistreview效率反而高了因为争议从我觉得变成了标准是。标准不是限制创造力的枷锁恰恰是减少无意义争论的润滑剂。另外我发现一个微妙但很重要的事用t3code的标准做review代码质量问题会提前暴露在合入前而不是上线后。以前三天两头在生产环境排查那些当时看着没问题后来发现是坑的代码现在这类问题明显少了。5.3 存量代码改造不要试图一夜之间推翻一切刚接触t3code的团队很容易有一个冲动把老代码全部重构一遍。我强烈建议别这么做。存量代码的体量往往比想象中大得多而且老代码通常还承载着复杂的业务逻辑贸然动刀很容易引入隐性bug。我的建议是把改造分成几步稳扎稳打先给存量项目配置好格式化工具和静态检查把机器能自动发现的问题先清零。从团队当前最痛的模块入手选定一个边界清晰、改动可控的模块做T1到T2级别的重构。建立绞杀者计划在新增代码中强制按t3code的标准走老代码保持冻结逐步替换。每次重构必须带着测试没有测试的模块先补关键路径的测试再动手改。我还记得我们第一次试点选了一个体量小但非常重要的支付回调模块。第一次重构改动量并不大主要是把原来五百行的函数按职责拆开把散落的日志和数据访问统一收拢。结果上线后出了个问题由于我们保留了原有行为不变定位问题时比对老代码日志反而更清晰——因为新代码的日志上下文更完整队友第一次体会到代码规范立刻转化为排障效率的正反馈。从那之后推t3code就不再是我一个人喊口号了而是变成大家主动想要的方向。4. 最后想说的t3code不是什么高深莫测的理论说穿了就是把很多优秀工程师已经在做的事情系统化、可执行化并且拆成了先能懂、再复用、再演进三个阶段。如果你从头建立一个新项目直接从第一级的标准开始成本几乎为零收益却会随着代码量增长而指数级放大。如果你手里已经有一堆存量代码也别焦虑从格式化工具和静态检查入手挑一个试点模块逐步改造体会小步重构带来的稳定感之后自然就知道怎么继续了。最后再分享一个小技巧把t3code的三级标准做成一张A4纸的checklist贴在工位上或者放进项目的CONTRIBUTING.md里。写代码之前扫一眼提交之前再扫一眼不出两周就会形成肌肉记忆。等哪天你看到一个命名精准、边界清晰、上线半年没出过问题的模块你会觉得当初定这套规矩花的时间真的太值了。
返回列表