先聊个现象。你打开 IDE,准备干活,结果终端在报警告:“UnicodeDecodeError”;去代码评审,同事甩过来一句“你这命名不符合 PEP8”;打开算法课设文档,题目又是“海明校验编码解码实验”。这三个场景里都出现了“编码”这个词,但说的根本不是一回事。软件工程领域里,术语从来不是拿来背的,是拿来定位问题的。你把“字符编码”“代码规范”“编码算法”混为一谈,排查方向就全错了;你搞清楚它们各自解决什么问题、背后是什么取舍,很多棘手问题其实一眼就能看穿。
这篇内容就按“编码与设计”这条线,把软件工程里高频出现、又容易被概念绕晕的术语梳理成一份实用笔记。适合软件工程专业的学生、刚入行的开发者,也适合准备面试或者正在做课设、写总结的人。我不会做成词典式罗列,而是按“这词到底解决什么问题”来讲,顺带把热搜里大家真正关心的那些点——UTF-8 中文占几个字节、海明码怎么算、设计模式期末到底在考什么、幂等性为什么是面试必问——用同一个框架串起来。
1. 字符编码:为什么“UTF-8”这个词能毁掉一个下午
1.1 “编码”在软件工程里至少有三个完全不同的含义
先把这个最害人的歧义拆开。日常语境下,中文里的“编码”至少对应三种不同的英文概念,而它们之间毫无归属关系:
| 中文说法 | 英文术语 | 实际解决什么问题 | 常见场景 |
|---|---|---|---|
| 字符编码 | Character Encoding | 把人类字符映射成计算机字节序列 | UTF-8、GBK、乱码、文件编码设置 |
| 编码规范 | Coding Standard | 统一代码风格,提升可维护性 | PEP8、Google Java Style、linter 配置 |
| 编码算法 | Encoding Algorithm | 把数据变换成压缩/纠错/加密后的形态 | Huffman、LZW、海明校验、Base64 |
最坑的就是第一种。字符编码处理的是“字符⇄字节”的转换规则,它不关心你的代码逻辑写得怎么样,但一旦搞错,轻则页面显示乱码,重则数据入库后永久损坏。热搜里那些“ajax 请求设置编码格式”“idea 设置文件编码”“为什么在 utf-8 编码中,中文字符通常占用的字节数比英文字符多”,全都掉进了这个坑。
1.2 同是“汉”,为什么在 UTF-8 里要占 3 个字节:手算一遍就懂
要理解中文在 UTF-8 里为什么“胖”,得先分清两个概念:码点和编码规则。
码点(Code Point)是 Unicode 为每个字符分配的全局唯一编号,比如“汉”的码点是 U+6C49;“A”的是 U+0041。但码点只是编号,它没有规定怎么存进字节。UTF-8 是一种编码规则,它把不同范围的码点用不同长度的字节串表示:
- U+0000 ~ U+007F(ASCII 区):用 1 个字节,格式
0xxxxxxx。 - U+0080 ~ U+07FF:用 2 个字节,格式
110xxxxx 10xxxxxx。 - U+0800 ~ U+FFFF(包含大部分常用汉字):用 3 个字节,格式
1110xxxx 10xxxxxx 10xxxxxx。
“汉”的码点是 U+6C49,落在第三个区间,所以必然占 3 个字节。具体怎么填?把 6C49 写成二进制:0110 1100 0100 1001,补成 16 位后按 4-6-6 拆成三段0110 110001 001001,分别填进三字节模板:
- 第一字节:
1110+0110=11100110= 0xE6 - 第二字节:
10+110001=10110001= 0xB1 - 第三字节:
10+001001=10001001= 0x89
所以“汉”在 UTF-8 下的实际字节是E6 B1 89。英文“A”码点是 U+0041,落在第一区间,直接就是0x41,1 个字节。
重点来了:这不是“中国人吃亏了”,而是 UTF-8 的设计选择。它用“变长 + 前导标志位”的方式兼容整个 Unicode 字符集,牺牲了部分字符的存储效率,换来了和 ASCII 的完全兼容。而 GBK 则直接给汉字分配 2 个字节,所以同样一个“汉”,在 GBK 下是 2 字节,在 UTF-8 下是 3 字节。你在项目里看到数据库字段长度设成 255,结果中文多了存不进去,根源就在这——255 通常按字节算,一个中文在 UTF-8 里顶 3 个英文。
1.3 文件编码、请求编码、数据库编码:乱码的三大窝点
字符编码相关的乱码问题,90% 出在三个环节。
第一个是文件编码。你在 IDE 里写了String s = "中文",保存文件时 IDE 按某种编码把源码写到磁盘;编译时编译器又按另一种编码读进来,只要两边不一致,源码里的字符串直接变成乱码。热词里“idea 设置文件编码”指的就是这件事。实操建议很简单:整个团队统一 UTF-8,并且在 IDE 设置里把项目编码、文件编码、控制台编码全部显式指成 UTF-8。注意,是“显式”,不要靠环境默认值,同一个源码在不同平台上的默认编码差异能让你玩一下午“为什么本地没事,CI 上就乱码”。
第二个是请求编码。一个是 URL 里的参数,另一个是请求体里的表单/JSON。“ajax 请求设置编码格式”这个热搜词对应的典型场景是:前端 POST 中文数据到后端,后端用 request.getParameter 拿到一串问号。这里牵涉的术语叫URL 编码(Percent-Encoding),它把非 ASCII 字符先编码成 UTF-8 字节,再把每个字节改写成%XX的形式。比如“汉”的三个字节E6 B1 89在 URL 里就是%E6%B1%89。所以你在浏览器地址栏里看到的一长串%E6%B1%89不是加密,只是转义。后端解析时如果用了错误的解码字符集,比如 Tomcat 默认按 ISO-8859-1 解 query string,中文自然全部变成???。
第三个是数据库编码。连接串里的characterEncoding=utf-8不是随便写的,它告诉 MySQL 驱动按什么编码把 Java 字符串转成字节流发给服务端。如果应用层是 UTF-8,数据库表和连接串是 latin1,写入时发生的转换就是你数据彻底损坏的开始——这种坏是回家作业级别的灾难,因为字符一旦以错误编码落库,再想还原就得靠业务层 guess,工作量巨大。
1.4 工程习惯:不要相信默认值,所有编码都显式声明
这条经验我愿称之为“字符编码防坑第一条”:凡是可以显式指定编码的地方,就不要依赖默认值。源码文件、HTTP 响应头、请求字符集、数据库连接串、MQ 消息体,全都显式写清楚。
还有个细节容易被忽略——BOM(Byte Order Mark)。UTF-8 的 BOM 是文件开头的EF BB BF三个字节,用来标记“本文件是 UTF-8”。Windows 下某些编辑器保存 UTF-8 文件时会自动加 BOM,Linux 工具链不认,结果就是你的 yaml 配置文件第一个字段名前面多了一个不可见字符,程序报“key not found”但肉眼死活看不出来。处理方式:项目内约定所有文本文件不使用 BOM;如果已经有带 BOM 的文件,用sed -i '1s/^\xEF\xBB\xBF//'一类的命令批量去掉。
另外提醒一句,热词里“路径编码(如 %2e%2e/)绕过限制访问静态资源”本质上是**规范化(Canonicalization)**问题,不属于字符编码问题,但很多前端把两者搞混。%2e%2e/解码后就是../,如果服务端在检查路径时没先做解码和归一化,攻击者就能绕过目录限制。这类问题正确的解决顺序是:先解码、再规范化、最后做权限检查,顺序反了就存在绕过风险。做安全设计的时候,这一个术语能帮你在评审会上少挨两次骂。
2. 代码规范类术语:PEP8、命名约定与静态检查工具
2.1 Coding Style、Coding Standard、Convention 的边界到底在哪
字符编码聊完了,进入“编码与设计”的第二层:规范的编码风格。先给三个词划界限:
- Coding Style:代码长什么样。缩进、空格、大括号换行方式、行长限制。它关乎观感和可读性。
- Coding Standard:代码必须遵守的规则。比如“禁止使用
eval”“所有返回集合的方法必须返回不可变集合”。它关乎正确性和安全性。 - Convention:团队内部约定。比如“新增需求必须在对应模块文档中同步更新”。它是流程层的默契。
这三个词在日常工作里经常被混着说,但定位完全不同。Style 是审美,Standard 是底线,Convention 是协作。你在简历上写“熟悉 PEP8 编码风格”没有任何问题,但如果你跟团队说“我按 PEP8 规范写代码”,潜台词其实是“我遵守了 Python 社区推荐的那套编码风格约定”,严格讲并不严谨,不过这属于语言习惯,知道差异就好。
2.2 为什么缩进和命名能决定你的 Code Review 体验
PEP8 全称是Python Enhancement Proposal 8,是 Python 官方的代码风格指南。它最常被引用的几条规则:
- 缩进用 4 个空格,不用 Tab;
- 每行最多 79 个字符(文档/注释建议 72);
- 类名用
CapWords,函数名/变量名用小写加下划线; - 模块内 import 顺序:标准库 → 第三方库 → 本地库,每组之间空一行。
如果你第一次看到这些规则觉得“这有什么好规定的”,我只能说,等你经历过一次 2000 行的函数、变量名从a到z1、缩进混用 Tab 和空格、提交代码时 diff 里全是空白的改动之后,你就懂了。代码是写给人看的,机器只负责执行。Code Review 的核心对象不是机器,是人。一份风格统一的代码,评审者能把精力放在“业务逻辑有没有问题”而不是“这行怎么缩进不对”;一份风格混乱的代码,评审效率和可维护性会成倍下降。
我自己踩过一个印象很深的坑:接手一个历史项目,发现某个方法被调用了十几次,每次调用都传 6 个参数,每次传参顺序还都不一样。问题不是没有类型检查,而是方法签名太烂。后来我按“函数不超过 4 个参数”的原则重构,把相关参数封装成配置对象,调用立刻清晰了。这类约束不属于 PEP8,但它在工程里实际比风格规定更重要——好的命名和签名设计,本身就是最有效的注释。
2.3 命名分层:包、模块、类、变量各有各的规则
命名规范是编码规范里最容易被低估的部分。很多新手觉得变量名长短无所谓,实际上命名是软件工程里少有的“零成本高收益”设计决策。我这里给一套工程通用的命名分层:
- 包/目录:全小写,尽量不用下划线,比如
com.company.project.service; - 模块/文件:全小写,可用下划线分隔,比如
order_service.py,Java 里通常是OrderService.java对应类名; - 类/接口:大驼峰
PascalCase,比如OrderService、UserRepository; - 函数/方法:小驼峰(Java)或下划线(Python),动词开头,比如
getOrderById、calculate_total_price; - 常量:全大写加下划线,比如
MAX_RETRY_COUNT; - 私有成员:Python 用单下划线前缀
_internal,Java 用private关键字,二者是不同维度的概念,别混。
命名还有个反直觉的经验:名字长短和清晰度不是正相关。太长让人扫读困难,太短又失去表意能力。判断标准是一个新读者在没看注释的情况下,能否猜出这个函数返回什么、有没有副作用。processData就是典型的废命名——什么数据?怎么处理?处理完返回什么?全是问号。parseConfigFromFile(path)就好得多,一眼看出输入输出。
2.4 把规范交给工具:linter、formatter、pre-commit 的一体化配置
规则再全,靠人脑盯是盯不住的。工程里的正确做法是把规范拆成两类,分别交给不同工具:
- Formatter(格式化器):自动改写代码风格。Python 用 Black,JavaScript 用 Prettier,Java 用 google-java-format。它解决“该不该换行、该不该加空格”的分歧,跑完以后团队所有人的风格强制统一。
- Linter(静态检查器):检查潜在 bug 和风格问题。Python 生态是 flake8 或 Ruff,JS 生态是 ESLint。它能发现“导入了但没使用”“变量未定义”“循环里修改了迭代变量”这些问题,比代码评审的人工眼力更靠谱。
推荐的工作流是三者配合:编辑器里装插件实时提示 → 保存时自动格式化为统一风格 → 提交前由 pre-commit 钩子强制跑一遍 linter,不通过不允许 commit → CI 里再跑一次同样的检查作为兜底。注意这里的关键词是“强制”。你要是只在本地跑,总有同事会忘记,最后风格还是乱。pre-commit 钩子是卡住流程的那道闸。
配置的时候有个小技巧:先让 formatter 统一格式,再让 linter 做规则检查,顺序反了格式化工具会不断覆盖 linter 的期望,导致误报满天飞。这个顺序踩过坑的人都懂,网上不少团队配置多轮流水线互相打架,就是因为没搞清楚这两类工具的分工边界。
3. 编码算法术语:海明校验、Huffman、LZW,课设到底在考什么
3.1 信息编码的三种动机:压缩、纠错、防错
聊完字符编码和代码规范,终于到算法课设里的那些“编码”了。这个领域的术语很多,但根本动机只有三类:
- 压缩编码:在不丢失信息的前提下,用更少的比特表示同样内容。代表:Huffman 编码、LZW 编码、算术编码、ZIP 里的 DEFLATE。
- 纠错编码:增加冗余比特,让接收方不仅能发现错误,还能定位并纠正错误。代表:海明码、Reed-Solomon、Turbo 码。
- 检错编码:增加冗余比特,只能发现有没有错,不能定位。代表:CRC(循环冗余校验)、奇偶校验、校验和。
这三个动机对应三种完全不同的工程场景。压缩是为了省空间/带宽;检错用在“发现错了就重传”的场景,比如 TCP 报文;纠错用在“重传代价太高”的场景,比如光盘划痕、内存条翻转、深空通信。热搜里的“海明校验编码解码实验”“matlab 实现 jpeg 压缩中的 huffman 编码”恰好覆盖了这三类里最容易混淆的两个——海明码是纠错,Huffman 是压缩,它们唯一的共同点是都叫“编码”。
3.2 海明校验:用冗余位定位错误的思想
海明校验的核心思想可以浓缩成一句话:用一组校验位的组合,把“错在哪一位”这个信息编码出来。
具体做法是给数据插入若干校验位,校验位的位置必须是 2 的幂次,也就是第 1、2、4、8、16 位……剩余的位放数据。假设要发送 4 位数据1011,加上校验位后一共有 7 位,第 1、2、4 位是校验位,第 3、5、6、7 位是数据位。每个校验位负责一组数据位的奇偶校验,分组的规则是:第 i 个校验位负责“位置编号的二进制表示中第 i 位为 1”的所有位。比如第 1 位校验 P1 负责位置 1、3、5、7;第 2 位 P2 负责位置 2、3、6、7;第 4 位 P4 负责位置 4、5、6、7。
接收方按同样规则重新计算每一组的奇偶校验,如果某一组校验结果不一致,把不匹配的校验位编号加起来,得到的就是出错的位号。比如 P1 和 P4 校验失败,P1 = 1,P4 = 4,出错位置就是 5,直接把第 5 位取反即可纠正。
这个设计最漂亮的地方在于:纠错能力和校验位的数量成对数关系。4 个校验位最多能定位 2^4 - 1 = 15 位数据里的错误,8 个校验位能覆盖到 255 位。ECC 内存条的原理就是它——内存里每个 64 位数据块配 8 位校验位,单比特翻转可以自动纠正,双比特错误至少能发现。你做课设的时候,如果只是抄代码就亏了,重点要体会“冗余位的数量”和“可定位位置集合的大小”之间的关系,这是信息论里 Hamming 距离的直观雏形。
3.3 Huffman 与 LZW:变长码和字典压缩
Huffman 编码解决的是“用最短的平均码长表示符号序列”的问题。它听起来很玄,核心思想只有一句:出现频率越高的符号,用越短的编码。
构造过程是:把所有符号按概率加入优先队列,每次取出概率最小的两个节点合成一个新节点,概率相加放回队列,重复直到只剩一棵树。叶子节点就是原始符号,从根到叶子的路径上的 0/1 串就是该符号的 Huffman 编码。比如一个文本里e出现概率最高,它可能只占 2~3 个比特;z出现概率极低,它的编码可能长达十几位。总体均值算下来,比固定 8 位一个字符省了接近一半空间。
JPEG、PNG、ZIP 里都有 Huffman 的影子,但通常不是单独使用,而是配合其它算法。比如 DEFLATE 就是“LZ77 + Huffman”:先用 LZ77 把重复出现的字符串替换成“距离 + 长度”对,再用 Huffman 把这种符号流进一步压缩。解题和面试的时候注意分清层次,别把“Huffman 编码”和“ZIP 压缩”画等号,后者是多种算法的组合。
LZW 编码则是完全不同的思路:字典压缩。它在编码过程中动态构建一个字典,把“当前遇到过的字符串”加到字典里,后续重复出现时直接用字典索引替代。GIF 和早期 TIFF 格式用的就是 LZW。这个算法的特点是:压缩和解压过程都不需要传输字典,两侧用同样的规则增量构建,这就是为什么 LZW 叫“自适应编码”。缺点也很明显——它只能发现完全相同的重复模式,对“稍有变化的重复”无能为力,所以后来被更灵活的变体算法取代。课设里做 lzw 时,最容易忽略的坑是字典初始化和溢出处理,记得给字典设上限并考虑重置策略,要不压缩到一半字典满了,后面的比率会很难看。
3.4 这些术语和真实系统的对应关系
算法课设里做完了,很多人会问一句:“这些以后真的用得上吗?”我的回答是:你不一定直接写这些算法的实现,但你每天都在用它们的结果。
- 你的浏览器从服务器拿到的 HTML 可能是 Brotli/gzip 压缩的,里面同时用到了 LZ 系列和 Huffman;
- 你的电脑内存用的是 ECC 的话,就是在用海明码家族的纠错思想;
- 你扫码支付时,二维码里的容错机制用的是 Reed-Solomon 编码,它是海明码思想在“多个错误定位”场景下的推广;
- 你在 Redis 里存的哈希键,内部可能用了不同编码策略,包括 zipmap 这类针对小数据量的压缩编码。
所以学这些编码算法,真正值钱的是“信息编码”这个抽象视角:到处都在做一件事——在冗余、时延、带宽之间权衡。压缩省带宽但是费 CPU,纠错加冗余但能省重传。你带着这个视角去看技术选型,看一个压缩库为什么选这个参数、看一个协议为什么加校验字段,就不会觉得它们是孤立知识点。
4. 设计模式:编码与系统设计之间的“共同语言”
4.1 23 个模式为何值得进入术语库
设计模式的话题在编程社区里永远吵不完,有人奉为圭臬,有人喊“过度设计”。我的观点比较折中:设计模式是代码层面和系统设计层面之间的共同语言,它的价值主要在沟通,而不在实现。
你想,两个工程师讨论一个订单模块怎么改造,如果 A 说“我想给支付环节加一层抽象,让不同渠道各自实现”,B 可能要花十分钟理解 A 的意图;但如果 A 说“这里我用策略模式”,B 马上知道 A 想让支付算法可以互相替换、调用方和实现方解耦。模式本质上是一种带上下文的简写,它让复杂的设计意图可以被压缩成几个词传递。
GoF 的《设计模式》总结了 23 个模式,分三大类:创建型(对象怎么创建)、结构型(类和对象怎么组合)、行为型(对象之间怎么交互)。考试和课设里最爱考的组合是:单例、工厂、策略、观察者。这几个模式应对的业务场景也最典型。
4.2 创建型模式:从单例到工厂的演进逻辑
单例模式保证一个类只有一个实例,并全局提供访问入口。典型场景:配置管理器、线程池、数据库连接池。但它也是最容易被滥用的模式——很多人把它当成“全局变量”的高级包装,结果状态被到处改,debug 到崩溃。我的建议是:确认“唯一实例”是硬需求,而不是省事需求时,再考虑用它。现代框架里,Spring 的 Bean 默认就是单例的,你写业务代码时很多情况下不需要自己实现单例;自己写的单例反而要小心线程安全和序列化破坏单例的问题。
工厂模式按进化程度分三档:简单工厂、工厂方法、抽象工厂。简单工厂用静态方法根据参数返回不同对象,比如PizzaFactory.create("cheese");工厂方法把对象的创建延迟到子类,让子类决定实例化哪个具体类;抽象工厂则更进一步,提供一个创建一族相关对象的接口,比如“美式风格界面工厂”能同时创建美式按钮、美式输入框。面试时考察的关键点是“这个工厂到底解决了什么变化”:简单工厂解决“创建逻辑集中”,工厂方法解决“类型选择逻辑可扩展”,抽象工厂解决“一组产品的匹配关系”。别把三者背成一团,它们的区别在于是“一个方法”、“一个类”还是“一族产品”。
4.3 结构型模式:适配器、代理、装饰器的对比
结构型模式研究的是对象之间怎么“拼装”更合理。这里最容易混淆的是三个名字很接近的模式:适配器、代理、装饰器。
| 模式 | 核心意图 | 典型场景 | 与其它模式的本质区别 |
|---|---|---|---|
| 适配器 Adapter | 把接口 A 转换成调用方期望的接口 B | 老系统接口换成新系统接口 | 改变的是“接口形态”,不增加行为 |
| 代理 Proxy | 控制对目标对象的访问 | 远程代理、懒加载、权限控制、AOP | 控制“什么时候/谁”访问目标 |
| 装饰器 Decorator | 动态给对象增加职责 | Java IO 流BufferedReader包FileReader | 增加“行为”,且可以多层叠加 |
一个让你印象深刻的例子:装饰器是层层包装的俄罗斯套娃,每层加一个新功能,最后调最外层时所有功能一起生效;代理则像一个前台接待,你接触不到真正的对象,所有请求都要过他那一关。用 Java IO 来记最形象:new BufferedReader(new InputStreamReader(new FileInputStream(file))),BufferedReader 就是给 InputStringReader 装饰了缓冲功能。这个例子背下来,面试里再被问“装饰器是什么”基本稳了。
4.4 行为型模式:策略与观察者如何收拾 if-else
行为型模式解决的是“对象之间的责任分配和通信”问题。日常代码里出现频率最高、也最容易上手的是策略模式和观察者模式。
策略模式把一组可互换的算法封装成独立策略类,让调用方可以在运行时选择。最经典的落地场景就是干掉长 if-else 链。举个例子,假设你有支付渠道选择逻辑:
if (channel.equals("wechat")) { // 微信支付逻辑 } else if (channel.equals("alipay")) { // 支付宝支付逻辑 } else if (channel.equals("unionpay")) { // 银联支付逻辑 }用策略模式重构后,定义一个PaymentStrategy接口,三个渠道各自实现一个类,再用一个工厂根据channel参数拿到对应策略,调用pay(order)即可。好处有三个:新增渠道不用改老代码,符合开闭原则;每个渠道的逻辑被隔离在自己的类里,出问题定位快;可以针对不同策略做单元测试而不影响其它渠道。
观察者模式定义对象之间的一对多依赖,一个对象状态变化时,所有依赖它的对象都会收到通知。事件总线、消息队列的发布订阅模型、GUI 里的监听器,全是它的应用场景。它在代码层面的价值是解耦:发布方不需要知道谁会响应,订阅方不需要知道谁在发布,两边只通过事件类型这一个契约通信。
但这里务必说句大实话:设计模式不是越多越好。如果一个类只有 20 行代码、只有一个实现,你硬套工厂模式,那就是过度设计。“设计模式期末”热词说明大家在背、在考,但真正的工程判断力表现在“知道什么时候不该用模式”上。我个人的经验法则是:至少出现两处以上重复结构,并且未来确实可能继续长出第三处时,才值得抽象。
5. 从编码到设计的高频工程术语:幂等、SOLID 与防腐层
5.1 幂等性:202 和 200 之间差的不只是一个状态码
面试后端岗位,十个有八个会问“接口幂等性怎么设计”。这个术语看着高冷,其实就是一件事:同一个请求执行一次和执行多次,结果完全一样。GET 请求天然幂等;POST 默认不是;PUT 应该是;DELETE 争议较大,一般建议设计成幂等。
为什么非幂等的接口会导致线上事故?经典的坑是:客户端提交订单时网络超时,前端自动重试,结果同一笔订单被创建了两次;或者支付回调重复推送,积分被加了两次。解决方案按强度从弱到强有几种:
- 唯一业务键 + 数据库唯一索引。订单表对
order_no建唯一约束,重复插入直接抛异常,接口捕获后返回“已存在”而不是报错。这是最简单也最可靠的办法。 - 状态机约束。订单状态只能是
CREATED → PAID → SHIPPED → COMPLETED,重复回调发现状态不是CREATED就丢弃。这个方法的关键是状态流转必须在一个事务/原子操作内完成,否则并发场景会漏。 - 乐观锁/版本号。更新时携带
version,SQL 里带WHERE version = ?,更新成功后版本号加一,影响行数为 0 说明已经被别的请求改过,需要重新读。 - 全局幂等号中间件。类似微信支付的
Idempotency-Key,客户端每次请求生成唯一 ID,服务端拿 ID 去查/写一张幂等表,重复请求直接返回第一次的结果。
你在设计系统时,幂等性不应该最后补,而应该在接口设计阶段就回答一个问题:这个操作如果没有幂等保护,用户手滑点两下会发生什么?凡是资金、积分、库存相关,必须幂等;凡是纯查询,天然幂等,不用过度设计。
5.2 SOLID:五个字母背后是十五个权衡
SOLID 是面向对象设计的五个原则首字母缩写,任何谈“设计”的术语库都绕不开它:
- S 单一职责原则:一个类只应该有一个引起它变化的原因。判断方法是描述这个类时只能用一个“因为”,如果用了“并且”,就拆。
- O 开闭原则:对扩展开放,对修改关闭。新增功能时尽量新增代码,而不是改动已验证的老代码。策略模式就是开闭原则的典型实践。
- L 里氏替换原则:子类必须能替换父类且不影响程序正确性。说白了就是继承别乱用——当你发现子类要重写父类所有方法还抛异常时,你俩不该是父子关系。
- I 接口隔离原则:客户端不应该依赖它不使用的接口。大而全接口的坏处是改动一处牵连所有实现者,拆成小接口各自演进。
- D 依赖倒置原则:依赖抽象,不依赖具体实现。高层模块不该直接依赖低层模块,两者都应该依赖接口;依赖注入(DI)和控制反转(IoC)就是为它服务的。
注意,这些原则是互相约束的,各自都有成本和副作用。单一职责过度细化会导致类爆炸,接口隔离过度会导致接口碎片化。所以工程上从来不是“遵守”SOLID,而是“权衡”SOLID。遇到争议的时候,我的习惯是回到代价和收益——这个抽象能不能减少未来 3 个月的维护成本?能,才值得做。
5.3 团队术语库怎么建:词条模板与日常维护
既然聊术语库,那就顺带讲讲团队怎么把一个术语库真正用起来,而不是只挂一个文档链接吃灰。根据我自己维护团队 wiki 和内部知识库的经验,单个词条应该包含以下字段:
- 术语名称:中文名 + 英文原名 + 常用别名;
- 一句话定义:不依赖上下文,任何新人都能看懂;
- 详细说明:原理、背景、解决什么问题;
- 使用场景:至少给一个真实例子;
- 常见误区:把团队里踩过坑的典型错误写进去;
- 关联术语:指向其它词条,形成知识网络;
- 最近更新人/时间:方便追溯。
日常维护比初始整理更重要。一招比较有用的方式是:Code Review 时发现一个新概念,顺手给术语库提一个 MR。比如你给同事解释“这个原因是因为 MySQL 的隔离级别是可重复读”,解释完就把它写成一篇短词条。这样术语库是在的工作流里长出来的,而不是某次集中治理“运动式”搭出来的。再配合一个季度评审会,把引用次数低、内容过时的词条合并或删掉,这个库就不会变成无人维护的死文档。
6. 工具与趋势:AI 编码助手时代,术语库本身也在进化
6.1 从“编码技能”到“编码 skills”,热搜关键词在提醒什么
今年热词里冒出一个有意思的变化:“编码 skills”开始和“设计模式”“软件工程课程设计”并列出现。我自己也在留意这个信号——“会写代码”正在从“写对”变成“描述对、选择对”。
传统软件工程里,术语库解决的是“人和人之间如何高效沟通”;AI 编码助手普及后,术语库还要解决“人和模型之间如何精确交流”。你在 prompt 里写“帮我优化这段代码”,和写“请把这段代码按策略模式重构,保持行为不变”,得到的输出质量和可用性完全是两个量级。前者模型只能猜你的意图,后者你给了它明确的架构约束。在这个意义上,术语不再只是“面试要背的名词”,而是你指挥编码工具时的精确坐标。
6.2 token、上下文窗口、补全:用 AI 编码前先搞懂这几个词
既然提到 AI 编码,有几个术语你需要先拎清,否则连“为什么不限制 token”这种话都没法讨论:
- Token:模型处理文本的最小单位,不等于单词。一个 token 可能是一个英文单词的一部分、一个汉字、一个标点。很多工具按 token 计费,所以“不限制 token”通常指的是计费层面不设上限,不是能力没有上限。
- 上下文窗口:模型一次能“看到”的 token 总量。窗口越大,它能记住的代码前后文越长,生成结果越贴合项目实际。超出窗口的历史代码会从注意力里消失,表现为“说着说着它忘了你之前的约定”。
- 代码补全 vs 代码生成:补全是基于当前上下文预测下一段,类似自动补全的加强版;生成是根据你的自然语言描述,产出整块代码。前者重在吻合现有风格,后者重在理解意图。
- 幻觉:模型生成看起来合理、实际错误的内容。它不会告诉你“这段代码我没验证过”,所以代码评审和测试在这里变得比人写代码时更重要。
用这些术语思考的好处是,你能快速判断一个 AI 编码工具到底哪里厉害、哪里会坑。比如你问“为什么不限制 token”,真正该关心的是模型上下文窗口多大、支不支持自动截断、截断策略会不会丢失关键声明。术语准确,讨论才不会被营销文案带着走。
6.3 为什么越是有 AI,人越需要精确的术语
有一点和直觉相反:工具越智能,使用者越需要概念清晰。以前你写代码,不懂“幂等性”最多是接口写得不稳,上线后还能修;现在你让 AI 生成代码,你不懂的术语,你连 prompt 都写不出来,更无法判断它输出的是不是正解。
我见过不少朋友让 AI 生成“订单支付接口”,结果代码里完全没有幂等控制;不是工具不行,是他们没在 prompt 里给出“请包含幂等设计”这个约束。工具只会在你给出的约束边界内优化,约束本身需要人来定。术语库的存在价值,就是帮你在下任何指令前,已经在脑子里把边界画清楚。
总结不出什么玄学,只分享一个我的习惯:每次看到新概念,多问一句“它想解决什么问题,代价是什么”。字符编码用冗余换兼容,海明码用冗余换纠错能力,设计模式用抽象换可维护性,幂等用冗余/状态换一致性——软件工程里几乎每个了不起的设计,都是这个框架下的一个决策。把这句话记心里,比背多少术语表都管用。