资讯详情

资讯详情

代码成熟度分级:从T1到T3,如何判断代码能否上生产

1. 我为什么盯上T3这个等级代码成熟度分层的由来1.1 用能跑和能交付之间差的不是一个调试器几个月前团队里一个后端服务连续出了三次线上事故每次根因都不复杂——无非是空指针、漏处理边界、日志打了半天定位不到参数。但在复盘时发现一个共性这几段代码在功能上都是正确的测试能过接口能通联调没问题。只是当数据稍微偏离理想输入时它就变得极其脆弱。我那时候开始认真想一个问题我们一直在用功能是否完成来评判代码质量但功能完成和代码可靠之间有一条很宽的沟。后来我参考了业内一些成熟度模型结合自己带团队的经验把代码分成三个等级T1、T2、T3并把这套方法起了个名字叫 t3code。它不是某种框架也不是某个工具而是一套代码成熟度判断标准——用来回答这段代码到底能不能上生产。T1代码的特点是场景正确在预期路径下能跑通变量命名随意函数偏长异常处理靠外层兜底测试以覆盖主要流程为主。T2代码的特点是路径完整主要分支和异常分支都写了模块边界清晰代码能被人接手维护。T3代码则是在T2基础上进一步做到可证明的可靠——任何人拿到代码不需要费劲推断就能明确知道它会在什么情况下做什么事出错时能快速定位修改时不会波及无关逻辑。1.2 T3的判定标准到底是什么很多朋友问我T3是不是就是代码写得规范一点不是。规范是基础但T3有一套更具体的检查维度我把它总结成五个问题可读性一个不熟悉这段代码的工程师能否在十分钟内说出主流程的输入、处理和输出边界完整性空值、超范围、并发冲突、下游超时——这些非理想情况是否都有明确的处理策略错误可观测性当代码出错时日志是否足够定位到具体的参数和上下文可测试性核心逻辑是否不依赖环境就能构造测试测试是否在防御未来可能出现的回归修改安全性改动一个需求点时改动范围是否被限制在局部而不是牵连其他调用方这五个问题就是 t3code 的核心骨架。下文我会逐个展开说清楚每一条在实际工作中到底怎么落地而不是停留在口号层面。2. 第一道坎可读性不是玄学是规则约束2.1 命名里的信息密度决定排查成本可读性很多时候是个主观词但一旦落到代码评审场景它就得有客观抓手。我评审代码时最先看命名。举个例子之前接手过一个支付回调模块代码里大量出现data、info、temp、res这类名字。最致命的是同一个方法里有三个不同的data分别是请求体、解析后的业务对象和数据库查询结果。一旦出现线上问题开发者自己都要先读一遍代码才能确认当前操作的是哪一层数据。每次排查少说多花半小时三个人接力下来就是半天。T3级的命名标准其实很朴素变量名要能回答这个值是什么比如paymentOrder、rawCallbackBody、parsedTradeResult而不是data。函数名要能回答这个方法做完会怎样比如generateRefundVoucher而不是doAction。布尔值要能回答这个条件为真意味着什么比如isCallbackSignatureValid而不是flag。有位同事说过一句话我印象很深命名不是在给编译器写说明是在给三个小时后的自己留线索。这句话后来直接写进了我们团队评审清单的第一条。2.2 函数短小与分支出口的可见性函数该多短没有绝对数字但有一个实操标准一个函数应该能在不滚动屏幕的情况下完整读完。如果超过三十行还全是if嵌套基本可以确定它做了不止一件事。T3对函数结构有两个硬性要求一个函数只保留一个抽象层级。比如判断订单状态、计算优惠金额、组装返回对象这三件事应该拆成三个函数而不是揉在一个长方法里。每个分支都必须有显式出口。这个要求主要在防御隐蔽路径。很多bug不是写在主路径上而是藏在某个else或者catch里默默吞掉导致问题隔了很久才暴露。我在拆解这类函数时常用一个笨办法把原来的长函数打印出来用笔把每个嵌套层级框起来凡是圈到第三层以上的一律拆出去。拆完之后代码量通常会变大但每个函数都变得可读、可测、可单独review。2.3 注解的态度T3下什么时候写注释我不反对注释但强烈反对把注释当赎罪券——代码写不清楚靠注释来解释。T3对注释的定位是解释为什么而不是复述是什么。下面这两种注释是我在评审时比较认可的# 这里不能直接复用缓存中的汇率因为支付请求发起时需要对冲锁定汇率 rate getLockedExchangeRate(order.currency, order.created_at)// 延迟重试三次避免上游网关瞬时抖动造成误判 for (let attempt 0; attempt 3; attempt) { try { const result await callExternalGateway(payload); return normalizeResult(result); } catch (err) { if (attempt 2) throw mapGatewayError(err); await sleep(200 * (attempt 1)); } }而下面这段代码是查询用户余额这种注释删掉反而更清爽。要检查自己写得是否合格可以做一次注释遮罩测试把代码中的注释全部遮住如果代码本身仍然能让你明白流程那注释是加分的如果代码本身看不懂那问题在代码结构不在注释。T3级的代码注释应该是一种补充而不是救命稻草。3. 第二道坎错误的处理方式决定代码的可靠性3.1 异常处理过度与不足哪个更危险T1代码最常见的问题是用一层层try/catch把异常吞掉。很多开发者觉得不让接口崩溃就等于处理了异常实际上是把问题从直接报错变成了静默失败。最典型的案例是一个订单模块catch块里只写了一条日志log.warn(error)连异常堆栈都没打印生产环境里日志量少得可怜但业务数据已经悄悄对不上了。反过来另一种极端是异常处理过度把正常的业务分支也抛异常。比如用户未登录时抛一个UserNotLoginException让全局拦截器处理这在架构上没问题但如果把参数缺失余额不足这类预期内分支也全部用异常流来表示代码的可读性和性能都会受影响。T3的异常处理遵循一个简单的原则预期内的分支用返回值或结果对象表达例如Result.fail(余额不足)。预期外的错误才用异常表达例如数据库连接失败、上游服务违反协议。每个catch必须做三件事之一重新抛出、包装后抛出、处理并明确返回。除此之外的catch都属于吞异常。3.2 边界参数与默认值策略宁可拒绝不要猜代码里有很多bug来自开发者替用户做决定。举个例子一个函数接收分期数installmentCount当传入0时代码默认改成12期。这个好心的行为在多数情况下没问题但当某个调用方误传了0系统会静默地给用户分了12期账单出来后才发现不对——这个锅很难追溯因为系统里没有痕迹表明这里做过默认值替换。T3的边界处理策略是对不合法输入显式拒绝对缺失值的处理必须有明确业务含义。def calculate_installment(principal: float, count: int) - dict: if count 0: raise ValueError(installmentCount must be positive, actual: {}.format(count)) if principal 0: raise ValueError(principal must be non-negative, actual: {}.format(principal)) # 业务逻辑...这里的关键是把不符合预期的输入挡在业务逻辑之前让错误在入口处就暴露出来。有时候一些同事觉得这样太啰嗦但恰恰是这几十行校验让线上问题从玄学变成了可复现的明确异常。3.3 从日志里反推T3要求有个很实用的反向检查法当你不知道代码怎么写时先想日志怎么打。如果一段代码将要产生的日志可以在出错时还原现场那这段代码大概率是T3级的反之如果日志打出来没人能定位到具体原因代码就需要重构。具体到字段我认为一条合格的错误日志至少要包含发生错误的具体操作createOrder、refundCallback当前操作涉及的核心参数订单号、用户ID、金额异常类型与堆栈上下文信息比如当前环境标识、调用来源以日志为参照很多设计问题会提前暴露。比如发现某个函数根本没有接收订单号那说明参数设计有问题发现需要打印的参数在函数作用域里不存在说明分层不合理。日志其实是最好的代码体检报告。4. 第三道坎测试覆盖率换成测试有效性4.1 什么才算有效测试很多团队引入覆盖率指标后测试数量上去了但质量反而下降了——大量测试只断言函数没抛异常或者为了覆盖分支而拼出一堆假数据。这类测试我把它们叫做安慰剂测试跑的时候全绿重构时全红稍微改一下实现就碎一地最后人人厌恶改测试。T3对测试的判定只有一句测试的价值在于暴露回归风险而不是凑覆盖率。所以我更看重三个问题测试是否验证了具体的输出和状态变化而不是只验证没报错测试是否包含了边界和异常输入测试失败时能否明确告诉你是哪个行为不符合预期下面是一个典型的安慰剂测试test(should not throw, async () { await client.createOrder({ userId: 1, amount: 100 }); // 没有断言建单失败也会通过 });而有效的测试长这样test(should create order with pending status, async () { const order await client.createOrder({ userId: 1, amount: 100 }); expect(order.status).toBe(PENDING); expect(await db.getOrderCountByUser(1)).toBe(1); });每一个断言都对应一个具体业务行为失败时能精确指出哪里偏离了预期。4.2 可测试性改造依赖注入与纯函数拆解测试难写多半不是测试的问题而是代码设计的问题。最常见的情况是业务逻辑里直接创建了数据库连接、调用了全局配置、依赖了时间函数导致测试必须在特定环境里才能跑。T3解决这个问题靠两个常用手段。第一个是依赖注入。核心函数不自己new依赖而是把依赖作为参数传入。这样测试时就能传入mock对象不用真的起服务。def process_refund(refund_request, payment_gateway): result payment_gateway.refund(refund_request) return result第二个是把纯逻辑与副作用分离。比如金额计算、状态判断、费率换算这类输入相同输出必相同的逻辑可以拆成纯函数。纯函数天然可测试不需要mock任何东西。我见过一个比较典型的改造原本一个下单接口里计算优惠和写数据库混在一个函数里测试要先造一堆数据库记录才能跑。后来我把优惠计算抽成独立的computeDiscount(items, coupons)写库逻辑留在原处。测试量少了一半覆盖却明显更扎实因为所有复杂的计算分支都能用纯数据直接驱动。4.3 测试的维护成本控制从测试全量到测试关键差异T3不要求单元测试覆盖一切但要求关键业务差异点覆盖到位。每个分支都是成本每行测试都要长期维护所以我会按优先级取舍优先级测试对象原因高金额计算、状态流转、费率换算出错直接影响资金和数据一致性高外部接口调用与重试逻辑依赖不稳定需要快速发现异常中权限规则鉴权属于安全类回归代价高低纯展示类、样板代码改动频率高测试价值有限按这个清单来分配精力团队不会陷入每个getter都要写测试的教条同时核心路径尽可能被测试网兜住。5. 一个T1函数重构到T3级的完整过程5.1 原始的一段能用代码为了把上面这些原则串起来我拿一个真实场景演示一个获取用户可用优惠券并计算最优折扣的函数。最初版本长这样功能能跑但问题不少def get_discount(user_id, order_items): data db.query(select * from coupons where user_id%s, user_id) total 0 for item in order_items: total item[price] * item[count] best 0 for c in data: if c[min_amount] total: if c[rate] best: best c[rate] if len(data) 0: return {discount: 0} return {discount: total * best, coupon_count: len(data)}第一眼看上去很流畅但拆开看至少存在这些隐患data这个名字没有体现优惠券列表的语义空数据判断放在最后逻辑顺序有点绕没有处理order_items为空的情况如果total为0discount会按0计算但有些优惠券的满减本质应该是满0减0——可能业务上不该使用任何券SQL注入先不谈单说代码可读性一个新人拿到这段代码要确认best到底是啥就要花时间。5.2 逐条列出的问题清单我按 t3code 的五个维度给原始代码做了体检维度问题可读性data命名过于笼统best含义不明确边界完整性order_items为空、user_id不存在、优惠券无可用券时都能跑但没有区分返回语义错误可观测性没有日志出问题后无法知道是哪张券哪条规则导致了最终结果可测试性函数直接依赖db查询测试必须起数据库修改安全性如果后续要新增优惠券类型叠加规则这个函数会变得越来越长5.3 重构后的代码与验证重构时我先拆分成三个纯函数查询券列表保留副作用在入口处调用、计算可用券、计算最优折扣。主函数只看成一个编排层def get_discount(user_id, order_items, coupon_repoNone): repo coupon_repo or default_coupon_repo user_coupons repo.get_usable_coupons(user_id) if not order_items: log_warn(order_items empty, user_id%s, user_id) return {discount: 0, coupon_count: 0} total sum(item[price] * item[count] for item in order_items) if total 0: return {discount: 0, coupon_count: len(user_coupons)} available [ c for c in user_coupons if c[min_amount] total and c[status] ACTIVE ] if not available: return {discount: 0, coupon_count: 0} best_rate max(c[rate] for c in available) discount total * best_rate log_info(discount calculated, user_iduser_id, totaltotal, best_ratebest_rate, coupon_countlen(available)) return {discount: discount, coupon_count: len(available)}同时把order_items为空的场景、无可使用券场景都写了单测。重构后代码量多了但每个分支都有明确出口日志能定位到关键参数测试可以在不依赖数据库的情况下直接喂数据跑。我要特别说明一点重构不是为了追求更花哨或更简短。只要这段代码有明确的边界策略、清晰的命名和可观测性它就已经达到了T3的门槛。好的重构不是把代码改得高深而是让代码遇到的问题一眼可见。6. 让团队接受T3code的分级与取舍6.1 分级评审代替一刀切如果团队里所有代码都必须按T3标准写进度大概率会崩。T3的意义不是说每段代码都必须是最高标准而是让团队清楚地知道当前代码处在哪个等级以及为什么值得升级。我推行 t3code 的时候采用了一种分级评审制度核心资金链路、鉴权、数据一致性模块必须T3才能合入。普通业务接口、报表查询达到T2即可合入预留T3重构的todo。一次性脚本、原型验证T1可接受但要在注释里明确标注一次性用途。评审时不再笼统说这段代码需要再改改而是直接说这段代码目前是T2距离T3缺边界处理和有效测试。这样开发者知道自己缺什么讨论也有了共同语言。6.2 T3的工时该如何估算很多团队排斥代码质量要求本质上是担心提质加班。我自己的经验是T3的额外成本主要集中在上线前的第一次重构而不是日常开发。第一次把代码从T1改成T3可能需要多花30%到50%的时间但之后每个需求的迭代因为逻辑清晰、测试有效反而会比T1模式更快更稳。估算时我会把工时拆成两块功能开发按原有估算不变T3补齐按边界处理日志核心路径测试来算通常比功能开发少一个量级如果某段代码在评审时被打回很可能不是补几行测试的问题而是设计阶段就少了约束。真正的低成本方案是写代码前先画清楚输入输出和边界分支这比写完再返工划算得多。6.3 什么时候允许停留在T2T2和T3之间确实存在灰色地带。我的取舍标准是看这段代码是否在未来三个月内会被修改。如果是一次性同步脚本、临时数据迁移、演示DemoT2完全够用如果是核心接口或长期维护模块停留T2意味着每一次需求变更都在积累风险那我宁可开发时多花一小时也不愿意上线后花三天排查。还有一个容易被忽视的点升级一批历史遗留代码时不要贪多。我会先挑一个变更最频繁、出过线上事故的模块做T3化改造跑通之后把收益讲给团队听比贴一百条规范文档都管用。t3code 真正能落地靠的不是命令而是让每个工程师在几个迭代之后感觉到这样写确实更省心。我在推行这套标准的过程中最大的体会是代码质量从来不是靠自觉而是靠一套透明的判断规则。把好代码从感觉变成可检查的条目团队才有机会真正向T3靠拢。至于那些暂时还留在T2的代码也不用心虚——标记清楚、排好债、在下次迭代顺便还掉这就是成熟团队该有的节奏。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →