1. 项目缘起与核心定位
第一次看到"t3code"这个名字,我下意识地把它拆成了"t3"和"code"两截。在开发者圈子里,这种命名方式其实挺常见——前缀往往代表某种技术栈、某个版本号,或者干脆就是作者随手起的一个短标识,后缀则直接点明用途。我翻了一圈社区里的讨论,发现大家对这个词的联想基本集中在几个方向:有人觉得它跟TypeScript有关,有人猜是某个代码生成工具的代号,还有人把它当成一套轻量级编码规范的简称。不管最初的含义是什么,这个词本身已经成了一个不错的切入点,让我可以聊聊围绕"代码"这件事,一个普通开发者到底能折腾出多少实用的东西。
我写这篇东西的出发点很简单:市面上讲代码规范、讲工具链的文章太多了,但大部分要么是官方文档的复述,要么是堆砌一堆配置让人照抄。我想换个方式,把我自己在实际项目里踩过的坑、试过的方案、最后沉淀下来的那套做法,原原本本地讲一遍。这套做法我内部就叫它"t3code"——三个核心原则加一套落地流程,不追求大而全,只求能跑通、能维护、能交接。
这篇文章适合谁看?如果你是一个正在带小团队的技术负责人,或者是一个独立开发者,手头有几个中小型项目需要长期维护,那你应该能从里面找到不少共鸣。如果你刚入行不久,对代码组织还处于"能跑就行"的阶段,那这篇文章可能会帮你省下不少后期重构的时间。我不打算讲什么高深的理论,全是实操层面的东西,你可以直接拿去用,也可以根据自己项目的情况做调整。
2. 为什么是"三个原则"而不是一堆规范
2.1 规范越多,执行越差
我待过一家公司,代码规范文档写了四十多页,从命名风格到注释格式到提交信息模板,事无巨细。结果呢?新项目前两周大家还照着做,第三周开始就有人图省事直接复制粘贴旧代码,一个月后整个仓库的风格就彻底放飞了。这件事给我的教训特别深:规范的数量和执行率之间,存在一个明显的反比关系。你定的规则越多,单条规则被记住的概率就越低,最后大家干脆全部放弃。
后来我自己带项目,就给自己定了一条死规矩:核心原则不超过三条。三条的好处是,任何人听完都能记住,不需要查文档。而且三条原则之间可以互相制衡,不会出现"遵守了A就违反了B"的情况。t3code里的"t3"其实就是这个意思——three principles,三个原则。至于具体是哪三个,我下面会展开讲,但你先记住这个逻辑:少即是多,能记住的规范才是好规范。
2.2 原则要能指导决策,而不是描述状态
很多规范的问题在于,它们描述的是"好代码长什么样",而不是"遇到选择时该怎么选"。比如"代码要清晰易读"这句话,听起来很对,但当你面对一个复杂的业务逻辑,有两种写法摆在面前,你该怎么判断哪种更"清晰"?这种规范就是无效的,因为它没法指导具体决策。
我定的三个原则,每一条都是一个决策框架。遇到分歧的时候,拿这三条去套,基本都能得出一个大家都能接受的结论。这比争论"哪种写法更好看"要高效得多。而且这三条原则是有优先级的,当它们冲突的时候,优先级高的那条说了算。这个优先级顺序本身也是经过好几次调整才定下来的,后面我会讲为什么这么排。
2.3 原则要能落地成检查项
光有原则还不够,你得有办法检查。我的做法是,每条原则都对应一组可以自动检查的规则。比如"命名要能自解释"这条原则,对应的检查项就是:变量名长度是否在合理区间、是否包含无意义的缩写、是否与同文件内其他命名风格一致。这些检查项可以写成lint规则,也可以做成代码审查时的检查清单。关键是,检查项必须具体到"是/否"的程度,不能有模糊地带。
我见过太多团队把规范挂在墙上,但代码审查的时候全靠 reviewer 的个人喜好。这种做法的结果就是,同一个问题,张三 review 的时候通过了,李四 review 的时候被打回来,搞得提交代码的人无所适从。有了明确的检查项,reviewer 只需要对照清单打勾,争议就少了很多。
3. 第一条原则:命名即文档
3.1 为什么命名排在第一位
如果让我从所有编码习惯里挑一个最重要的,我会毫不犹豫地选命名。原因很简单:代码里出现频率最高的元素就是各种名字——变量名、函数名、类名、文件名、参数名。这些名字构成了阅读代码时的第一印象。一个糟糕的命名,会让读者在理解代码逻辑之前,先花大量时间去猜测"这个变量到底是干嘛的"。
我做过一个粗略的统计,在一个中等规模的项目里,开发者阅读代码的时间大约是编写代码时间的三到五倍。而阅读过程中,超过一半的困惑都来自命名不当。换句话说,你在命名上多花一分钟,可能帮未来的自己和其他人省下十分钟。这个投入产出比,比任何性能优化都要高。
3.2 命名的三个层次
我把命名分成三个层次来要求,从低到高分别是:准确、具体、自解释。
准确是最低要求。变量名要能正确反映它存储的内容,函数名要能正确反映它执行的操作。听起来很简单,但实际项目中,"data""info""temp""result"这类词满天飞。这些词的问题在于它们太泛了,读者看完之后只知道"这里有个东西",但不知道这个东西是什么。我的做法是,在代码审查时看到这类词,直接打回去要求重命名。一开始大家会觉得麻烦,但坚持两周之后,整个团队的命名质量会有明显提升。
具体是第二层要求。比如一个函数叫"processData",它确实准确——它确实在处理数据。但"处理"这个词太模糊了,是过滤、排序、聚合还是转换?读者必须去看函数体才能知道。更好的命名是"filterActiveUsers"或者"sortByCreateTime",这样读者不看实现就能知道这个函数干什么。具体化的关键在于,用动词精确描述操作,用名词精确描述对象。
自解释是最高要求。一个好的命名,应该让读者不需要看上下文就能理解它的含义。比如"userList"和"activeUserList",后者就比前者更自解释,因为它隐含了"只包含活跃用户"这个信息。再比如"calculateTotalPrice"和"calculateTotalPriceWithTax",后者明确说明了是否含税,避免了调用方的猜测。自解释的命名往往会长一些,但这点长度换来的是阅读效率的提升,非常值得。
3.3 命名的常见陷阱与规避方法
第一个陷阱是缩写。很多开发者为了省几个字符,把"button"写成"btn",把"manager"写成"mgr",把"configuration"写成"cfg"。这些缩写在你写代码的当下可能觉得很顺手,但过两个月你自己回来看,可能都要愣一下才能反应过来。我的建议是,除了行业公认的缩写(比如"id""url""http"),其他一律写全称。现在的编辑器都有自动补全,多打几个字符的成本几乎可以忽略。
第二个陷阱是拼音和英文混用。这个在中文开发者里特别常见,比如"getYongHuList"这种。这种命名的问题在于,它既不符合英文习惯,也不符合中文习惯,读起来非常别扭。我的做法是,要么全英文,要么全拼音,绝对不混用。如果英文实在想不出合适的词,用拼音也比混用好,至少风格是统一的。
第三个陷阱是数字后缀。比如"user1""user2""user3",这种命名在临时脚本里还能忍,但在正式项目里就是灾难。读者看到"user2"的时候,完全不知道它和"user1"的区别是什么。正确的做法是用有意义的限定词来区分,比如"adminUser""guestUser""vipUser"。
3.4 命名检查的自动化方案
光靠人工审查命名,效率太低。我的做法是配置一套lint规则,把常见的命名问题自动检测出来。比如用ESLint的"id-length"规则限制变量名长度,用"camelcase"规则强制驼峰命名,用自定义规则检测黑名单词汇(如"data""info""temp")。这些规则可以在提交代码时自动运行,不通过就拒绝提交。
对于更复杂的命名问题,比如"是否自解释",自动化工具很难判断。我的做法是维护一个团队内部的命名词典,把项目中常用的业务概念和对应的标准命名列出来。新人在命名之前先查词典,找不到再自己起名,起完名之后补充到词典里。这样既保证了命名的一致性,又降低了新人的学习成本。
4. 第二条原则:函数只做一件事
4.1 单一职责的实操定义
"函数只做一件事"这个说法,相信很多人都听过。但什么叫"一件事"?这个定义太模糊了。我见过有人把一个函数拆成五个小函数,结果每个函数只有两行代码,调用链长得像迷宫。这不是单一职责,这是过度拆分。
我的定义是这样的:一个函数只做一件事,意味着你能够用一句不含"并且"的话来描述它的功能。比如"验证用户输入"是一件事,"验证用户输入并且保存到数据库"就是两件事。这个定义的好处是,它给了你一个明确的判断标准。当你发现描述函数功能的时候需要用"并且"来连接,那就说明这个函数该拆了。
但这里有个前提:拆出来的子函数必须是有意义的。如果拆出来的函数只是把原来的一行代码包了一层,那这种拆分就没有价值。判断标准是,拆出来的函数是否可以被独立测试、是否可以被复用、是否有明确的输入输出。如果三个答案都是"否",那就不值得拆。
4.2 函数长度的合理区间
关于函数长度,业界有很多说法,有人说不超过20行,有人说不超过50行。我的经验是,不要死守一个数字,而是看这个函数是否容易理解。一个30行的函数,如果逻辑是线性的、命名清晰的,读起来可能比一个10行的嵌套函数更轻松。
不过,有一个信号值得警惕:当你需要滚动屏幕才能看完一个函数的时候,这个函数大概率太长了。我的做法是,把函数长度控制在"一屏之内",大概40到60行。超过这个长度,就考虑拆分。拆分的依据不是行数,而是逻辑层次。比如一个函数里既有参数校验、又有业务处理、又有结果组装,那就可以按这三个层次拆成三个函数。
4.3 参数设计的讲究
函数的参数列表,是很多人容易忽略的地方。我见过一个函数有八个参数,调用的时候要对着文档一个一个填,填错一个就出bug。这种函数的设计就有问题。
我的原则是,参数不超过四个。超过四个的时候,考虑两种方案:一是把相关的参数合并成一个对象,二是重新审视这个函数是不是做了太多事。合并成对象的做法特别适合那些总是成对出现的参数,比如"startDate"和"endDate",把它们合并成一个"dateRange"对象,既减少了参数数量,又明确了这两个参数之间的关系。
另外,参数的类型要尽量具体。比如一个函数接收"id"参数,如果这个id是用户id,那就命名为"userId",类型也应该是"UserId"而不是"string"。这样调用方在传参的时候,类型系统就能帮他检查出错误。这个做法在TypeScript项目里特别有效,我强烈建议所有新项目都上TypeScript,光是参数类型检查这一项,就能省下大量调试时间。
4.4 副作用的隔离与处理
纯函数是理想状态,但实际项目里,完全不产生副作用的函数几乎不存在。读写数据库、调用接口、修改全局状态,这些都是副作用。我的做法不是消灭副作用,而是隔离副作用。
具体来说,我会把业务逻辑和副作用分开。业务逻辑写成纯函数,输入输出明确,方便测试。副作用则集中在一个薄层里,比如一个repository层负责数据库操作,一个service层负责接口调用。这样业务逻辑的测试不需要mock任何东西,直接传参调函数看返回值就行。而副作用层的代码因为很薄,测试起来也简单。
这个做法还有一个好处,就是当副作用需要替换的时候,改动范围很小。比如从MySQL换成PostgreSQL,只需要改repository层的实现,业务逻辑完全不用动。这种架构上的清晰,在项目初期可能感觉不到好处,但到了中期需要换技术栈或者做重构的时候,优势就非常明显了。
5. 第三条原则:错误要早暴露
5.1 为什么错误处理比错误预防更重要
很多开发者把大量精力花在"预防错误"上,比如做各种参数校验、加各种边界判断。这当然没错,但我的经验是,错误处理比错误预防更重要。原因在于,你永远无法预防所有错误。网络会断、磁盘会满、第三方接口会挂,这些都不是你能控制的。与其试图预防一切,不如确保错误发生的时候能被及时发现、快速定位。
"早暴露"的核心意思是:错误发生的位置,要尽可能接近错误产生的位置。比如一个参数校验失败,应该在函数入口就报错,而不是等到这个参数被用到的时候才报错。前者你能立刻知道是调用方传错了,后者你可能要排查半天才能找到源头。
5.2 快速失败的具体做法
快速失败(fail fast)是"早暴露"的具体实现。我的做法是,在每个函数的入口处做参数校验,不合法就直接抛异常。这个异常要包含足够的信息:哪个参数不合法、期望什么类型、实际收到什么值。这样调用方看到异常信息,立刻就知道问题出在哪里。
对于异步操作,快速失败同样适用。比如一个接口调用,如果返回的状态码不是200,应该立刻抛异常,而不是继续往下走。我见过很多代码,接口调用失败之后不报错,继续用undefined往下算,最后在一个完全不相干的地方报了一个莫名其妙的错误。这种调试体验非常糟糕。
5.3 错误信息的编写规范
错误信息是给谁看的?很多人下意识觉得是给用户看的,所以写得很委婉,比如"操作失败,请稍后重试"。但实际上,错误信息的第一读者是开发者。用户看到的应该是友好的提示,而开发者看到的应该是详细的错误信息。
我的做法是,错误信息里必须包含三个要素:什么操作失败了、失败的原因是什么、可以怎么解决。比如"读取配置文件失败:文件不存在,请检查路径是否正确"。这样的错误信息,开发者一看就知道该去检查文件路径。而用户看到的则是另一套文案,比如"系统配置加载失败,请联系管理员"。
在代码层面,我会区分两种错误:一种是可预期的业务错误,比如"用户不存在",这种错误用自定义的Error类来抛,携带错误码和详细信息;另一种是不可预期的系统错误,比如"数据库连接失败",这种错误直接抛原始异常,让上层去决定怎么处理。这个区分很重要,因为业务错误通常需要展示给用户,而系统错误通常需要记录日志并告警。
5.4 日志与错误的配合
错误要早暴露,但暴露之后得有记录。我的做法是,在错误抛出的地方不打日志,在错误被捕获处理的地方打日志。这样做的原因是,错误在抛出的时候,往往还没有足够的上下文来判断严重程度。而到了捕获处理的地方,已经知道这个错误对业务流程的影响了,这时候打日志才能打出有价值的信息。
日志的级别也要讲究。业务错误用warn级别,系统错误用error级别。warn级别的日志不需要立刻处理,但需要定期回顾,看看是不是有频繁发生的业务异常。error级别的日志则需要立刻关注,通常要配告警。我见过一些项目,所有错误都打error级别,结果告警天天响,大家就麻木了,真正的严重问题反而被淹没。
6. 从原则到落地:一套可执行的检查流程
6.1 提交前的自检清单
原则再好,不执行也是白搭。我的做法是,在代码提交之前,让开发者自己过一遍检查清单。这个清单不用太长,五到十条就够了,对应三条原则的具体检查项。比如:
- 变量名是否准确、具体、自解释?
- 函数是否只做一件事?能否用一句不含"并且"的话描述?
- 参数是否超过四个?超过的话是否合并成了对象?
- 错误是否在发生位置附近被抛出?
- 错误信息是否包含操作、原因、解决方案三要素?
这个清单我会放在项目的README里,也会做成提交模板的一部分。开发者提交代码的时候,模板会自动带出这个清单,他需要逐项确认。一开始大家会觉得繁琐,但养成习惯之后,整个过程也就多花一两分钟,换来的是代码质量的明显提升。
6.2 代码审查的聚焦点
代码审查最怕的就是漫无目的地看。我的做法是,审查者只关注三件事:命名是否达标、函数职责是否单一、错误处理是否到位。其他方面,比如代码风格、格式问题,全部交给自动化工具去处理。这样审查者的精力集中在真正重要的事情上,审查效率会高很多。
审查意见的写法也有讲究。我要求审查者不说"这里不好",而是说"这里违反了哪条原则,建议怎么改"。比如"这个函数名'processData'不够具体,建议改成'filterActiveUsers',因为它实际做的是过滤活跃用户"。这样的意见,提交者知道该怎么改,也知道为什么要改,下次遇到类似情况就能自己判断了。
6.3 自动化工具的配置要点
自动化工具是执行原则的保障。我的项目里通常会配置这几类工具:格式化工具(如Prettier)负责统一代码风格,lint工具(如ESLint)负责检查命名和潜在错误,类型检查工具(如TypeScript)负责检查类型安全,测试工具(如Jest)负责验证业务逻辑。
这些工具的配置有一个原则:规则要少而精。我见过有人把ESLint的所有规则都打开,结果代码里全是红色波浪线,开发者干脆把lint关掉了。我的做法是,只开启与三条原则直接相关的规则,其他规则一律关闭。这样lint报出来的问题都是真正需要关注的,开发者也不会觉得被工具绑架。
6.4 新人上手的引导流程
新人加入项目的时候,最怕的就是面对一堆规范不知道从哪下手。我的做法是,给新人安排一个"引导任务":让他用t3code的原则去重构一个小模块。这个模块不用太复杂,一两百行代码就够了。重构的过程中,他会自然地理解三条原则的含义,也会遇到各种具体问题,这时候再给他讲规范,效果比干巴巴地念文档好得多。
引导任务完成之后,我会让新人做一次分享,讲讲他在重构过程中遇到了哪些问题、是怎么解决的。这个分享既是检验,也是让老成员回顾原则的好机会。我试过几次,效果都不错,新人上手速度明显比放养式快很多。
7. 常见问题与排查技巧实录
7.1 命名相关的高频问题
问题一:想不出合适的英文名怎么办?
这是中文开发者最常遇到的问题。我的建议是,先查团队内部的命名词典,看看有没有现成的。如果没有,可以用在线词典查,但要注意查到的词是否准确。如果实在找不到合适的词,用拼音也比用错误的英文好。另外,可以养成收集命名的习惯,看到好的命名就记下来,时间长了就有自己的命名库了。
问题二:命名太长影响可读性怎么办?
长命名确实会影响可读性,但前提是它真的长到影响阅读了。我见过有人把"getUserById"改成"getUser",理由是后者更短。但"getUser"的含义不明确,是获取当前用户还是根据id获取用户?这种为了短而牺牲明确性的做法,得不偿失。我的判断标准是,如果命名长度超过30个字符,才考虑简化。30个字符以内的命名,明确性优先。
问题三:团队命名风格不统一怎么办?
这个问题通常出现在多人协作的项目里。我的做法是,在项目初期就定好命名风格,写进README,并且配置lint规则强制检查。如果项目已经进行到一半才发现风格不统一,那就先统一新代码的风格,旧代码在重构的时候逐步改。不要试图一次性改完所有旧代码,那样风险太大。
7.2 函数拆分相关的高频问题
问题一:拆出来的函数太多,调用链太长怎么办?
这是过度拆分的典型症状。我的判断标准是,如果一个函数的调用链超过三层,就要考虑合并一些中间层。合并的原则是,把那些只被调用一次、逻辑简单的函数合并回调用方。但要注意,合并之后函数不能太长,如果合并后超过60行,那就说明拆分本身是合理的,问题出在别的地方。
问题二:函数之间有共享状态怎么办?
共享状态是函数拆分的大敌。我的做法是,把共享状态显式地作为参数传递,而不是通过闭包或者全局变量来共享。这样每个函数的输入输出都是明确的,测试起来也方便。如果参数太多,就把共享状态打包成一个context对象,作为第一个参数传递。这个做法在React的useReducer里很常见,效果很好。
问题三:异步函数怎么保持单一职责?
异步函数确实容易变得复杂,因为要处理各种回调、Promise链。我的做法是,把异步操作和业务逻辑分开。异步操作放在一个薄层里,只负责发起请求和返回结果。业务逻辑写成同步的纯函数,接收异步操作的结果作为输入。这样业务逻辑的测试不需要处理异步,简单很多。
7.3 错误处理相关的高频问题
问题一:错误信息太详细会泄露敏感信息怎么办?
这是个好问题。我的做法是,错误信息分两层:一层是给开发者看的详细版,包含堆栈、参数值等;另一层是给用户看的简化版,只包含必要的提示。在代码里抛错误的时候,抛详细版;在展示给用户之前,转换成简化版。这个转换通常在一个统一的错误处理中间件里做,不需要每个地方都写。
问题二:错误被吞掉了怎么办?
错误被吞掉是调试的噩梦。我的做法是,在代码审查时特别关注catch块。如果catch块里只有一行console.log,或者干脆是空的,直接打回去要求处理。处理的方式可以是重新抛出、可以是转换成业务错误、可以是记录日志并返回默认值,但绝对不能什么都不做。
问题三:错误日志太多,找不到重点怎么办?
日志太多通常是因为打日志的位置不对。我的做法是,只在错误被处理的地方打日志,不在错误抛出的地方打。另外,日志要分级,业务错误用warn,系统错误用error。定期回顾warn日志,看看有没有频繁发生的业务异常需要优化。error日志则要配告警,确保严重问题能被及时发现。
7.4 工具配置相关的高频问题
问题一:lint规则太严,开发者抵触怎么办?
规则太严确实会引起抵触。我的做法是,只开启与核心原则相关的规则,其他规则一律关闭。另外,规则的严重级别可以调整,有些规则设为warning,不阻塞提交;有些规则设为error,阻塞提交。这样既保证了核心原则的执行,又不会让开发者觉得被过度约束。
问题二:自动化工具运行太慢怎么办?
工具运行慢通常是因为检查的范围太大。我的做法是,只检查变更的文件,而不是整个项目。这个可以通过配置lint工具和测试工具来实现。另外,可以把检查放在提交前的钩子里,而不是每次保存都运行。这样既保证了检查的执行,又不会影响开发效率。
问题三:工具配置在不同开发者机器上不一致怎么办?
这个问题通常是因为配置没有纳入版本管理。我的做法是,把所有工具的配置文件都提交到仓库里,包括lint配置、格式化配置、编辑器配置。另外,用Docker或者类似的容器化方案来统一开发环境,确保每个人用的工具版本一致。这样就能避免"在我机器上能跑"的问题。
8. 一些个人体会
这套t3code的做法,我前后在三个项目里用过,每次都会根据项目情况做一些调整。最大的感受是,原则要少,执行要严。三条原则听起来简单,但真正坚持下来并不容易。特别是在项目赶进度的时候,很容易就想"这次先这样,下次再改"。但经验告诉我,这种妥协一旦开始,就会越来越多,最后原则就形同虚设了。
另一个感受是,工具是辅助,人才是核心。再好的lint规则,也检查不出命名是否真正自解释;再完善的检查清单,也替代不了代码审查时的判断。所以我在团队里一直强调,原则是给大家一个共同的判断框架,而不是替代思考。遇到原则覆盖不到的情况,大家讨论决定,然后把结论补充到检查清单里。这样原则本身也在不断进化,越来越贴合项目的实际情况。
最后分享一个小技巧:我会在项目里维护一个"反例集",把违反原则的代码片段收集起来,配上说明和修改建议。新人入职的时候,先看这个反例集,比看正面示例学得快。因为人天生对错误更敏感,看到反例的时候会想"哦,原来这样写是不行的",印象比看正面示例深刻得多。这个反例集我一般放在项目的wiki里,每次代码审查发现典型问题就补充进去,时间长了就成了一本很实用的教材。