1-1-3-能不用就不用的工程态度
1.1.3 "能不用就不用"的工程态度
引言
在软件工程中,有一个反直觉的真理:最好的代码是不存在的代码。
这不是说不写代码,而是说:在能用更简单的方式解决问题时,不要引入复杂的解决方案。
这个原则适用于:
- 依赖库
- 框架
- 设计模式
- 中间件
- 微服务
- 任何"看起来很酷"的技术
每一个依赖都是一个负债
案例:一个看似无害的依赖
假设你需要生成一个 UUID:
// 方案1:引入库
npm install uuid
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// 方案2:原生实现
const id = crypto.randomUUID(); // Node.js 原生支持(v14.17+)
方案1的隐藏成本:
- 包大小:+10KB
- 依赖安全漏洞的风险
- 需要定期更新维护
- 构建时间增加
node_modules多了1个目录
方案2的成本:
- 0 额外依赖
- 原生代码,性能更好
- 不需要维护
但你可能会说:"才10KB而已,有什么关系?"
问题在于:这种想法会传染。
- 今天加一个 10KB 的 UUID 库
- 明天加一个 50KB 的日期格式化库(但其实只用了其中一个函数)
- 后天加一个 200KB 的工具库(只用了其中的 3 个函数)
三个月后,你的项目:
package.json有 87 个依赖node_modules有 500MB- CI 构建时间从 2 分钟变成 10 分钟
- 有 15 个安全漏洞警告
- 没人敢升级依赖,因为不知道会破坏什么
依赖选择的判断标准
何时应该引入依赖
✅ 应该引入依赖的情况:
-
核心业务复杂度高
# 不要自己实现 JWT 加密 import jwt token = jwt.encode(payload, secret, algorithm='HS256')加密、安全相关的代码,自己写容易出错。
-
社区标准解决方案
// 不要自己实现 HTTP 客户端 import axios from 'axios';像 HTTP 请求这种有大量边界情况的功能,用成熟的库。
-
实现成本远大于维护成本
# 不要自己实现 PDF 生成 from reportlab.pdfgen import canvas某些功能的实现需要几个月,而维护依赖只需要偶尔更新。
❌ 不应该引入依赖的情况:
-
功能简单,几行代码就能实现
// 不需要引入 lodash 只为了用 _.isArray // 原生就有 Array.isArray(value) // 不需要引入库只为了 debounce function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } -
只用到库的一小部分功能
// 不要为了一个函数引入整个 lodash import _ from 'lodash'; // 70KB _.capitalize('hello'); // 只引入需要的部分 import capitalize from 'lodash/capitalize'; // 2KB // 或者自己实现 const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1); // 0KB -
依赖不活跃或维护者单一
检查:- 最后一次提交是什么时候?(超过1年 = 红色警报)
- 有多少维护者?(只有1个人 = 风险)
- Issue 回复速度如何?
- 有多少未解决的安全漏洞?
真实案例:left-pad 事件
2016年,一个只有11行代码的 npm 包 left-pad 被作者删除,导致成千上万的项目无法构建。
left-pad 的完整实现:
function leftPad(str, len, ch) {
str = String(str);
ch = ch || ' ';
while (str.length < len) {
str = ch + str;
}
return str;
}
无数项目依赖了这个 11 行代码的库,而不是花 2 分钟自己写一个。
教训:不要为了"避免重复造轮子"而引入微小的依赖。
"能不用就不用"适用的其他场景
1. 设计模式
错误示例:为了用设计模式而用设计模式
// 一个简单的配置类
public class Config {
private static Config instance;
private String apiKey;
private Config() {}
public static synchronized Config getInstance() {
if (instance == null) {
instance = new Config();
}
return instance;
}
// ... getter/setter
}
问题:
- 在现代框架中,依赖注入容器已经管理了单例
- 这个手动单例反而增加了测试难度
- 如果未来需要多个配置实例怎么办?
更简单的做法:
// 就用一个普通类
public class Config {
private String apiKey;
public Config(String apiKey) {
this.apiKey = apiKey;
}
// ... getter/setter
}
// 在需要单例时,用框架的依赖注入
@Singleton
public class Config { ... }
2. 微服务
不是所有项目都需要微服务。
微服务适用场景:
- 团队规模 > 50人
- 不同模块有不同的扩展需求
- 有成熟的 DevOps 基础设施
微服务不适用场景:
- 团队 < 10人
- 项目刚起步,业务边界不清晰
- 没有专职的运维团队
真实故事:
某创业公司,5 个开发,做了一个用户量只有 1000 的产品,却拆分成了 12 个微服务:
- 用户服务
- 订单服务
- 支付服务
- 通知服务
- ...
结果:
- 本地开发需要启动 12 个服务
- 新人入职培训需要 2 周
- 一个简单的功能需要跨 3 个服务修改
- 部署一次需要 30 分钟
- 调试一个 bug 需要查 5 个服务的日志
6 个月后,他们合并回了单体应用,开发效率提升了 3 倍。
3. 数据库/中间件
不要在不需要时引入 Redis/Kafka/Elasticsearch。
常见对话:
新手:"我们应该用 Redis 做缓存。"
老手:"为什么?"
新手:"因为 Redis 很快。"
老手:"我们现在有性能问题吗?"
新手:"呃...没有。"
老手:"那为什么要加?"
引入新技术的标准:
- 有明确的问题(不是"可能有问题")
- 现有方案无法解决
- 收益远大于复杂度成本
示例:
| 场景 | 不要用 | 先用这个 |
|---|---|---|
| 简单键值缓存 | Redis | 内存哈希表 + 定期清理 |
| 消息队列(低并发) | Kafka | 数据库表 + 轮询 |
| 全文搜索(数据量<1万) | Elasticsearch | 数据库 LIKE 查询 |
| 配置管理 | Etcd/Consul | 配置文件 + 热重载 |
不是说这些技术不好,而是说在问题出现之前引入它们是过度设计。
对使用 AI 的程序员的特别建议
AI(特别是 ChatGPT/Claude)会倾向于推荐"业界标准"的解决方案,这些方案往往是复杂的、完整的、企业级的。
例子
你的问题:"如何在 Node.js 中读取配置文件?"
AI 的回答:
// 安装 dotenv, config, joi 等库
npm install dotenv joi
// 创建配置验证 schema
const Joi = require('joi');
const schema = Joi.object({
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
// ...
});
// 加载并验证配置
const { error, value: config } = schema.validate(process.env);
if (error) throw error;
module.exports = config;
其实你需要的:
// 就用原生 JSON
const config = require('./config.json');
或者:
// 环境变量,不需要额外的库
const config = {
port: process.env.PORT || 3000,
dbHost: process.env.DB_HOST || 'localhost'
};
如何向 AI 提问以得到简单方案
❌ 差的提问:
"如何实现用户认证?"
AI 会给你完整的 JWT + Refresh Token + OAuth 方案。
✅ 好的提问:
"我的项目是一个内部工具,只有10个用户,如何实现最简单的用户认证?不要用额外的库。"
实践原则
1. 三问原则
在引入任何新东西(库、框架、中间件、设计模式)之前,问:
- 我现在有什么具体问题?(不是"未来可能有")
- 更简单的方案能解决吗?(原生功能、自己写几行代码)
- 引入它的长期成本是什么?(维护、学习、调试)
2. 80/20 法则
如果一个库有 100 个功能,但你只用其中 5 个,考虑:
- 只引入需要的部分(如 lodash 的单个函数)
- 或者自己实现这 5 个功能
3. 删除的勇气
定期审查依赖:
# 查看哪些依赖实际上没被用到
npm install -g depcheck
depcheck
# 查看各个依赖的大小
npm install -g cost-of-modules
cost-of-modules
发现不再需要的依赖,立即删除,不要犹豫。
4. 渐进式引入
不要一开始就用最复杂的方案。
Version 1: 配置文件(JSON)
↓ (发现需要环境变量)
Version 2: 环境变量(.env)
↓ (发现需要验证)
Version 3: 配置 + 验证(简单的 if 检查)
↓ (发现验证逻辑复杂)
Version 4: 配置库(joi/yup)
不要直接跳到 Version 4。
反例:过度依赖的项目
真实案例(已脱敏):
一个简单的 Todo 应用,前端代码的 package.json:
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"redux": "^4.0.0",
"react-redux": "^8.0.0",
"redux-thunk": "^2.4.0",
"redux-saga": "^1.2.0", // 同时用了 thunk 和 saga
"reselect": "^4.1.0",
"immutable": "^4.0.0",
"lodash": "^4.17.0",
"moment": "^2.29.0",
"axios": "^1.0.0",
"react-router-dom": "^6.0.0",
"styled-components": "^5.3.0",
// ... 还有 30 个
}
}
这个应用做什么?
- 一个本地使用的 Todo List
- 不需要服务器
- 功能:添加、删除、标记完成
它需要这么多依赖吗? 不需要。
简化后的版本:
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
用 React Hooks 管理状态,用 LocalStorage 持久化,用原生 Fetch API 请求(如果真的需要的话)。
结果:
- 包大小从 2.5MB 减少到 300KB
- 构建时间从 45 秒减少到 8 秒
- 新人理解代码的时间从 2 天减少到 2 小时
总结
"能不用就不用"的核心思想:
- 每一个依赖都是负债 —— 引入成本低,长期维护成本高
- 优先考虑原生方案 —— 语言/框架自带的功能优先
- 简单实现 > 完美实现 —— 先用最简单的方案,等问题出现再优化
- 对 AI 的建议保持批判 —— AI 倾向于"标准方案",而不是"适合你的方案"
- 定期清理 —— 删除不再需要的依赖
记住:你引入的每一个依赖、每一个抽象、每一个中间件,都是在给未来的自己挖坑。 有些坑是必要的,但大部分是可以避免的。
好的工程师不是知道所有技术的人,而是知道什么时候不用技术的人。

浙公网安备 33010602011771号