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 个安全漏洞警告
  • 没人敢升级依赖,因为不知道会破坏什么

依赖选择的判断标准

何时应该引入依赖

✅ 应该引入依赖的情况

  1. 核心业务复杂度高

    # 不要自己实现 JWT 加密
    import jwt
    token = jwt.encode(payload, secret, algorithm='HS256')
    

    加密、安全相关的代码,自己写容易出错。

  2. 社区标准解决方案

    // 不要自己实现 HTTP 客户端
    import axios from 'axios';
    

    像 HTTP 请求这种有大量边界情况的功能,用成熟的库。

  3. 实现成本远大于维护成本

    # 不要自己实现 PDF 生成
    from reportlab.pdfgen import canvas
    

    某些功能的实现需要几个月,而维护依赖只需要偶尔更新。

❌ 不应该引入依赖的情况

  1. 功能简单,几行代码就能实现

    // 不需要引入 lodash 只为了用 _.isArray
    // 原生就有
    Array.isArray(value)
    
    // 不需要引入库只为了 debounce
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }
    
  2. 只用到库的一小部分功能

    // 不要为了一个函数引入整个 lodash
    import _ from 'lodash'; // 70KB
    _.capitalize('hello');
    
    // 只引入需要的部分
    import capitalize from 'lodash/capitalize'; // 2KB
    
    // 或者自己实现
    const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1); // 0KB
    
  3. 依赖不活跃或维护者单一
    检查:

    • 最后一次提交是什么时候?(超过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 很快。"
老手:"我们现在有性能问题吗?"
新手:"呃...没有。"
老手:"那为什么要加?"

引入新技术的标准

  1. 有明确的问题(不是"可能有问题")
  2. 现有方案无法解决
  3. 收益远大于复杂度成本

示例

场景 不要用 先用这个
简单键值缓存 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. 三问原则

在引入任何新东西(库、框架、中间件、设计模式)之前,问:

  1. 我现在有什么具体问题?(不是"未来可能有")
  2. 更简单的方案能解决吗?(原生功能、自己写几行代码)
  3. 引入它的长期成本是什么?(维护、学习、调试)

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 小时

总结

"能不用就不用"的核心思想:

  1. 每一个依赖都是负债 —— 引入成本低,长期维护成本高
  2. 优先考虑原生方案 —— 语言/框架自带的功能优先
  3. 简单实现 > 完美实现 —— 先用最简单的方案,等问题出现再优化
  4. 对 AI 的建议保持批判 —— AI 倾向于"标准方案",而不是"适合你的方案"
  5. 定期清理 —— 删除不再需要的依赖

记住:你引入的每一个依赖、每一个抽象、每一个中间件,都是在给未来的自己挖坑。 有些坑是必要的,但大部分是可以避免的。

好的工程师不是知道所有技术的人,而是知道什么时候不用技术的人。

posted @ 2025-11-29 21:54  Jack_Q  阅读(0)  评论(0)    收藏  举报