重构——哎呀,我一直做错了。

2025-06-09

重构——哎呀,我一直做错了。

这篇文章最初出现在JustinDFuller.com上。

欢迎我的发言。我是个重构爱好者,而且我也不怕承认,但只有一个问题:我一直在做反了。你看,我的做法更准确地说是过早的代码抽象。

我们都知道重构。如果你读过哪怕一本编程书籍,或者花了很多时间阅读代码博客,你肯定对它有所耳闻。重构是一个重要的概念,它能让代码更易于理解、更易于维护、更易于扩展。

至少每个人都这么告诉我。

那么为什么重构没有实现我所期望的结果呢?

在编写我最新的库时,我花了一些时间反思代码的演进。我意识到,在产品完全可用、单元测试得出理想输出结果之前,我把代码重构成了一些我甚至不确定是否需要的接口。我移动了代码,使其可扩展、可复用,但为什么呢?这些代码最终能给出我需要的输出吗?我还不知道。

最终一切都顺利解决了,但我的代码是否比实际需要的更复杂?我认为是的。

原则高于目的

你听说过SOLID原则吗?我尽量严格遵循这些原则。我编写的每个函数都力求做到“单一职责”。我的类和工厂函数则力求对扩展开放,同时避免修改。我还尽量避免直接依赖太多东西,而是将依赖项作为函数和类的参数。

这算是优秀代码的秘诀吗?我想是的。当我的代码专注于 SOLID 或纯粹性,而不是完成它天生要做的事情时,问题就出现了。当我把原则置于目的之上时,问题就出现了。

例如,我一直非常注重确保我的单元测试没有昂贵的IO(输入和输出)。有时,由于错误地模拟了依赖项,我不得不回头修复错误的代码。

那么,解决方案是什么?

还记得我之前提到的反思吗?它让我想起了那句咒语:“让它运转起来,让它正确,让它快速。 ” 我意识到自己一直乱了套。我一直在让它正确,让它快速,然后让它运转起来!

让它发挥作用

随着我写作的增多,我越来越清楚地认识到,好的写作并非凭空而来。首先,我必须把所有的想法都写在纸上。我必须看清我的思绪会带我走向何方。然后,我必须把它们整理成某种半连贯、不散漫的版本,以表达我刚刚流露出的思想。

代码也会发生同样的事情。

把所有东西都放到那个函数里。一开始不用担心命名、单一职责或可扩展性——等你的函数运行起来后,你再考虑这些问题。需要明确的是,你不会像这样编写整个应用程序,而只是编写其中的一小部分。

一旦获得了所需的输出(您已经有单元测试来证明代码正确,对吧?),就可以开始重构,但不要太快太深!目前,请坚持以下重构策略:正确命名、只做一件事的函数以及避免代码突变;在确定重复模式之前,不要立即开始创建可扩展或可重用的类和工厂。

此时,使用任何具有逻辑优势的重构都是有意义的。这意味着重构的目的是为了使代码更容易理解,或者使代码更可靠。

考虑推迟使用仅在某些情况下有用的模式进行重构。

您会想要保存它们直到您有理由为止。

有一个理由

拥有 SOLID 代码不是一个理由。拥有函数式代码或纯代码也不是理由。

为什么要让代码可扩展?这样类似但不完全相同的功能就可以从基础逻辑中分支出来。

为什么要反转依赖关系?这样业务逻辑才能被多种实现使用。

希望您明白我的意思。有些重构本身就有意义。例如,重构变量的名称使其更准确总是有意义的。它的优点是与生俱来的。将函数重构为纯函数通常是有意义的,因为副作用可能会导致无法预料的问题。这是一个合理的理由。

“使用依赖倒置是最佳实践”不是一个理由。“好的代码是可扩展的”也不是理由。如果我只有几个不变的依赖项怎么办?我还需要依赖倒置吗?也许现在还不需要。如果我的代码不需要扩展,而且我也没有计划这样做怎么办?我的代码是否应该仅仅为了勾选这个选项就增加其复杂性?不!

请看下面的例子。

// not extensible

function getUser() {
  return {
    name: 'Justin',
    email: 'justinfuller@email.com',
    entitlements: ['global', 'feature_specific']
  }
}

// used later

getUser().entitlements.includes['feature_specific']

// Extensible

class User {
  constructor() {
    // initialize here
  }

  hasEntitlement(expectedEntitlement) {
    return this.entitlements.includes(expectedEntitlement)
  }
}

// used later

new User().hasEntitlement('feature_specific')
Enter fullscreen mode Exit fullscreen mode

你更喜欢哪个?你自然会先写哪个?当然,User 类的扩展性更强,因为它可以被其他类覆盖。例如,如果你有一个,那么你可以像这样SuperUser实现:hasEntitlement

hasEntitlement() {
  return true
}
Enter fullscreen mode Exit fullscreen mode

不要让 Class 搞砸了。没有它也能达到同样的效果。

function superUser(user) {
  return {
    ...user,
    hasEntitlement() {
      return true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

无论哪种方式,这种封装都hasEntitlement允许用户针对不同的用例利用多态性来扩展(而不是改变)代码。

尽管如此,该 User 类可能完全是多余的,而且现在您的代码比它需要的更复杂。

我的建议是,坚持使用最简单的模式,直到你找到更复杂的理由。在上面的解决方案中,你可以选择坚持使用同一个简单的用户数据对象,直到拥有多种用户类型。

复杂度顺序

现在,如果你允许的话,我要编个东西!我把它叫做“复杂度顺序”,它能帮我做重构决策。它看起来是这样的:

每当我决定如何组织功能时,我都会参考这个列表。我会选择能够满足我实现需求的最高级别选项。除非它实在无法工作,否则我不会再次选择。有时性能会影响这个选择,但这种情况并不常见。

通常,我会把一些东西放在对象里,而不是简单的常量变量里。或者,当我只需要一个函数时,我会创建一个工厂。

这份清单让我脚踏实地,防止我过早地进行重构。

平衡

我最近听说,如果你在会议上说“一切都是为了找到平衡”,每个人都会对你这句毫无意义的话点头,仿佛你说了什么深刻的话。我得赶紧试试。

不过,我认为平衡很重要。作为程序员,我们必须在代码质量、性能、可维护性以及完成任务的初衷之间取得平衡。

我们必须保持警惕,确保这两种需求都各得其所。如果我们的代码不能正常工作,就无法维护。另一方面,让糟糕的代码正常工作也很难。

尽管如此,代码仍然可以重构,但如果重构程度超出了有用的程度该怎么办?这些都是需要牢记的重要问题。

下次写代码的时候,请务必重构!但或许……不必重构?


这是一条转发帖子,该帖子最初出现在www.justindfuller.com上。


大家好,我是 Justin Fuller。很高兴您阅读我的帖子!我需要说明的是,我在这里写的所有内容均为我个人观点,不代表我雇主的任何立场。所有代码示例均由我本人编写,与我雇主的代码完全无关。

我也很乐意听取您的意见,请随时通过GithubTwitter与我联系。再次感谢您的阅读!

鏂囩珷鏉ユ簮锛�https://dev.to/justindfuller/refactoring-oops-i-ve-been-doing-it-wrong-33ge
PREV
我如何提高 CSS 水平
NEXT
使用 SSR 时,使用 HttpOnly Cookie 在 Next.js 中检测客户端身份验证