3、代码坏味道(下):全局数据、可变数据、发散式变化、霰弹式修改、依恋情结

好,咱们接着聊代码坏味道。上一章讲了几个常见的,这一章我挑了几个在实际项目中特别容易踩坑的。说实话,这些坏味道往往不是一眼就能看出来的,它们藏得比较深,但破坏力一点都不小。

我个人习惯把这一章的内容叫做「慢性毒药」——它们不会立刻让你的代码崩溃,但会慢慢腐蚀整个系统的可维护性。你想想看,一个项目维护了半年之后,改一个功能要改十几个文件,那种感觉,嗯,懂的都懂。

全局数据:看似方便,实则灾难

全局数据,说白了就是谁都能改、谁都能读的数据。我见过太多项目,一开始图省事,把一些配置、状态、缓存全扔到全局变量里。结果呢?项目一大了,你根本不知道这个变量在什么时候被谁改过。

⚠️ 警告: 全局数据是耦合的根源。它让代码的依赖关系变得隐式,你无法通过函数签名看出一个函数到底依赖了哪些数据。

举个例子,假设你有一个全局的 userSession 对象:

// 坏味道:全局数据
let userSession = {
  userId: null,
  token: null,
  role: 'guest'
};

function loadUserProfile() {
  // 这里隐式依赖了 userSession
  fetch('/api/user/' + userSession.userId);
}

function checkPermission() {
  // 这里也依赖了 userSession
  if (userSession.role === 'admin') { ... }
}

我在项目中遇到过最离谱的一次,是一个全局的配置对象被三个不同的模块修改,最后线上出了问题,查了两天才发现是某个模块在初始化时覆盖了配置。从那以后,我对全局数据就特别敏感。

💡 建议: 能用参数传递就用参数传递,能用依赖注入就用依赖注入。实在需要共享状态,考虑用不可变数据或者显式的状态管理。

可变数据:改着改着就乱了

可变数据和全局数据经常是一对「好兄弟」。可变数据的问题在于,你无法保证一个对象在传递过程中没有被修改。尤其是当你把对象传给一个函数,函数内部偷偷改了它的属性——这种「副作用」是 bug 的温床。

为什么会这样?因为人脑很难追踪一个可变对象的所有引用路径。你想想看,一个对象被传了五层函数,中间任何一层改了它,后面的逻辑都可能出问题。

// 坏味道:可变数据
function processOrder(order) {
  // 这里直接修改了传入的对象
  order.status = 'processing';
  order.updatedAt = new Date();
  // ... 其他逻辑
}

// 调用方
const myOrder = { id: 1, status: 'pending' };
processOrder(myOrder);
// 调用方根本不知道 myOrder 已经被改了

我曾经在一个项目中,因为一个可变对象在循环中被反复修改,导致最终结果完全不对。调试的时候发现,每次循环打印出来的对象都不一样——嗯,那种感觉,就像你在追一个永远追不上的影子。

✅ 推荐做法: 尽量使用不可变数据。如果必须修改,返回一个新对象,而不是修改原对象。JavaScript 里可以用展开运算符、Object.assign 或者 Immutable.js。

发散式变化:一个类,多个理由去改它

发散式变化,指的是一个类因为不同的原因而需要被修改。说白了,就是职责不单一。一个类如果同时承担了数据存储、业务逻辑、UI 渲染等多个职责,那它就会成为「修改热点」。

我见过一个类,里面既有数据库查询,又有 JSON 序列化,还有日志记录。每次需求变更,不管是改数据库、改格式还是改日志级别,都得动这个类。你说累不累?

变化原因 应该修改的类 实际修改的类(坏味道)
数据库表结构变更 UserRepository UserService(同时处理了 DB、序列化、日志)
JSON 格式变更 UserSerializer UserService
日志级别调整 Logger UserService

解决发散式变化的方法很简单:把不同的变化原因分离到不同的类中。每个类只因为一个原因而改变。这就是单一职责原则的核心思想。

霰弹式修改:改一个功能,改一堆文件

霰弹式修改和发散式变化是「镜面关系」。发散式变化是一个类被多个原因修改,霰弹式修改是一个原因导致多个类被修改。

举个例子,假设你要修改某个业务规则,结果发现需要改五个不同的文件:一个配置文件、一个验证逻辑、一个数据库查询、一个前端展示、一个日志记录。这就是典型的霰弹式修改。

⚠️ 注意: 霰弹式修改意味着相关的逻辑被分散到了多个地方。你应该把它们收拢到一个模块里。

我记得有一次,团队要改一个支付流程中的税率计算。结果发现税率逻辑散落在订单模块、发票模块、报表模块和财务模块里。改了一个地方,漏了另一个地方,线上对账差了十几万。嗯,从那以后,我们就把所有税率计算收拢到了一个 TaxCalculator 类里。

依恋情结:一个类总想拿别人的东西

依恋情结,指的是一个方法对另一个类的数据或方法特别「依恋」。说白了,就是某个方法频繁地访问其他类的数据,而不是自己类的数据。

为什么会这样?有时候是因为职责划分不合理。比如,一个 Order 类里有一个方法,大部分时间都在操作 Customer 类的数据。那这个方法其实应该搬到 Customer 类里去。

// 坏味道:依恋情结
class Order {
  constructor(customer) {
    this.customer = customer;
  }

  // 这个方法大部分时间都在操作 customer 的数据
  getCustomerDiscount() {
    if (this.customer.isVip) {
      return this.customer.vipDiscount;
    }
    if (this.customer.totalSpent > 10000) {
      return 0.1;
    }
    return 0;
  }
}

// 改进:把方法移到 Customer 类中
class Customer {
  getDiscount() {
    if (this.isVip) {
      return this.vipDiscount;
    }
    if (this.totalSpent > 10000) {
      return 0.1;
    }
    return 0;
  }
}

我个人习惯是,写代码的时候多问问自己:「这个方法放在这里合理吗?它是不是更依赖另一个类的数据?」如果答案是肯定的,那就搬过去。别犹豫。

知识体系总览

下面这张图帮你梳理了本章五个坏味道的核心逻辑和关系。你可以把它当作一个快速参考。

代码坏味道(下):五大核心问题 🌐 全局数据 谁都能改,谁都查不清 🔄 可变数据 副作用难以追踪 📦 发散式变化 一个类,多个修改理由 🔫 霰弹式修改 一个修改,改一堆文件 ❤️ 依恋情结 总想拿别人的数据 核心原则:高内聚、低耦合、单一职责 识别坏味道 → 重构 → 持续改进

避坑指南

最后,分享几个我自己的经验教训:

  • 我曾经在一个项目中,为了「方便」,把数据库连接信息写在了全局配置里。结果测试环境和生产环境的配置混在一起,差点把测试数据刷到生产库。从那以后,全局配置一律用环境变量 + 显式注入。
  • 我曾经写过一个工具类,里面全是静态方法,到处修改传入的对象。后来同事接手维护,每次改代码都要把所有调用点检查一遍。嗯,后来我重构成了纯函数,世界清净了。
  • 我曾经遇到一个霰弹式修改的场景,改一个字段名要改 12 个文件。后来我们引入了统一的 DTO(数据传输对象),所有字段映射集中管理,修改成本降到了 1 个文件。

说白了,这些坏味道的本质都是「耦合」。全局数据是数据耦合,可变数据是时间耦合,发散式变化和霰弹式修改是职责耦合,依恋情结是逻辑耦合。解耦,就是重构的核心目标。

好,这一章就到这里。记住,识别坏味道只是第一步,动手重构才是关键。下一章我们聊聊具体的重构手法,到时候见。


公众号:蓝海资料掘金营,微信deep3321