The Wrong Abstraction   错误的抽象

I originally wrote the following for my Chainline Newsletter, but I continue to get tweets about this idea, so I'm re-publishing the article here on my blog. This version has been lightly edited.
我最初为我的 Chainline Newsletter 写了以下内容,但我不断收到关于这个想法的推文,所以我在这里的博客上重新发布了这篇文章。这个版本经过了轻微编辑。


I've been thinking about the consequences of the "wrong abstraction." My RailsConf 2014 "all the little things" talk included a section where I asserted:
我一直在思考“错误的抽象”所带来的后果。我在 RailsConf 2014 的“所有的小事”演讲中包含了一个部分,我在其中断言:

duplication is far cheaper than the wrong abstraction
重复远比错误的抽象便宜

And in the summary, I went on to advise:
在总结中,我继续建议:

prefer duplication over the wrong abstraction
宁愿重复,也不要错误的抽象

This small section of a much bigger talk invoked a surprisingly strong reaction. A few folks suggested that I had lost my mind, but many more expressed sentiments along the lines of:
这个在整个演讲中篇幅不大的部分,却引起了出乎意料的强烈反响。有些人认为我疯了,但更多的人表达了类似这样的观点:

The strength of the reaction made me realize just how widespread and intractable the "wrong abstraction" problem is. I started asking questions and came to see the following pattern:
这次反应的强烈程度让我意识到“错误的抽象”问题是多么普遍和棘手。我开始提问,并看到了以下模式:

  1. Programmer A sees duplication.
    程序员 A 发现了重复的代码。

  2. Programmer A extracts duplication and gives it a name.
    程序员 A 提取了重复的代码并给它命名。

    This creates a new abstraction. It could be a new method, or perhaps even a new class.
    这创建了一个新的抽象。它可能是一个新的方法,甚至可能是一个新的类。

  3. Programmer A replaces the duplication with the new abstraction.
    程序员 A 用新的抽象替换了重复代码。

    Ah, the code is perfect. Programmer A trots happily away.
    啊,代码完美了。程序员 A 开心地走开了。

  4. Time passes.  时间流逝。

  5. A new requirement appears for which the current abstraction is almost perfect.
    出现了一个新需求,而当前的抽象几乎能满足这个需求。

  6. Programmer B gets tasked to implement this requirement.
    程序员 B 接到了实现此需求的任务。

    Programmer B feels honor-bound to retain the existing abstraction, but since isn't exactly the same for every case, they alter the code to take a parameter, and then add logic to conditionally do the right thing based on the value of that parameter.
    程序员 B 感到有义务保留现有的抽象,但由于它并非在所有情况下都完全相同,因此他们修改了代码以接受一个参数,然后添加逻辑以根据该参数的值有条件地执行正确的操作。

    What was once a universal abstraction now behaves differently for different cases.
    曾经普遍适用的抽象现在针对不同情况表现出不同的行为。

  7. Another new requirement arrives.
    又一项新需求到来了。

    Programmer X.  程序员 X。
    Another additional parameter.
    另一个附加参数。

    Another new conditional.  另一个新的条件。
    Loop until code becomes incomprehensible.
    循环直到代码变得难以理解。

  8. You appear in the story about here, and your life takes a dramatic turn for the worse.
    你出现在故事的这个节点,你的生活发生了戏剧性的转折,变得越来越糟。

Existing code exerts a powerful influence. Its very presence argues that it is both correct and necessary. We know that code represents effort expended, and we are very motivated to preserve the value of this effort. And, unfortunately, the sad truth is that the more complicated and incomprehensible the code, i.e. the deeper the investment in creating it, the more we feel pressure to retain it (the "sunk cost fallacy"). It's as if our unconscious tell us "Goodness, that's so confusing, it must have taken ages to get right. Surely it's really, really important. It would be a sin to let all that effort go to waste."
现有的代码会产生强大的影响。它的存在本身就说明它是正确且必要的。我们知道代码代表着付出的努力,我们非常有动力去保留这份努力的价值。而不幸的是,残酷的真相是,代码越复杂、越难以理解,也就是说,投入的创造成本越深,我们就越感到保留它的压力(“沉没成本谬误”)。这就像我们的潜意识告诉我们:“天哪,这太令人费解了,肯定花了很长时间才弄对。它肯定非常非常重要。让所有这些努力白费是一种罪过。”

When you appear in this story in step 8 above, this pressure may compel you to proceed forward, that is, to implement the new requirement by changing the existing code. Attempting to do so, however, is brutal. The code no longer represents a single, common abstraction, but has instead become a condition-laden procedure which interleaves a number of vaguely associated ideas. It is hard to understand and easy to break.
当你出现在上面第 8 步的故事中时,这种压力可能会迫使你继续前进,也就是说,通过修改现有代码来实现新的需求。然而,尝试这样做是残酷的。代码不再代表一个单一的、通用的抽象,而是变成了一个充满条件判断的过程,交织着许多模糊关联的想法。它难以理解,而且容易出错。

If you find yourself in this situation, resist being driven by sunk costs. When dealing with the wrong abstraction, the fastest way forward is back. Do the following:
如果你发现自己处于这种境地,请抵制沉没成本的驱动。在处理错误的抽象时,最快的进步方式是退回去。请执行以下操作:

  1. Re-introduce duplication by inlining the abstracted code back into every caller.
    通过将抽象化的代码重新内联到每个调用者中,来重新引入重复。
  2. Within each caller, use the parameters being passed to determine the subset of the inlined code that this specific caller executes.
    在每个调用者内部,使用传递的参数来确定内联代码的子集,该子集由该特定调用者执行。
  3. Delete the bits that aren't needed for this particular caller.
    删除该特定调用者不需要的部分。

This removes both the abstraction and the conditionals, and reduces each caller to only the code it needs. When you rewind decisions in this way, it's common to find that although each caller ostensibly invoked a shared abstraction, the code they were running was fairly unique. Once you completely remove the old abstraction you can start anew, re-isolating duplication and re-extracting abstractions.
这消除了抽象和条件,并将每个调用者简化为仅需要其代码。以这种方式回溯决策时,通常会发现,尽管每个调用者表面上都调用了一个共享的抽象,但它们运行的代码却相当独特。一旦完全移除旧的抽象,您就可以重新开始,重新隔离重复项并重新提取抽象。

I've seen problems where folks were trying valiantly to move forward with the wrong abstraction, but having very little success. Adding new features was incredibly hard, and each success further complicated the code, which made adding the next feature even harder. When they altered their point of view from "I must preserve our investment in this code" to "This code made sense for a while, but perhaps we've learned all we can from it," and gave themselves permission to re-think their abstractions in light of current requirements, everything got easier. Once they inlined the code, the path forward became obvious, and adding new features become faster and easier.
我曾见过一些团队,他们拼命地想在错误的抽象上前进,但收效甚微。添加新功能变得异常困难,每一次成功都让代码更加复杂,这使得添加下一个功能更加困难。当他们将观点从“我必须保留我们在这段代码上的投资”转变为“这段代码曾经有意义,但也许我们已经从中吸取了所有能学到的东西”,并允许自己根据当前的需求重新思考他们的抽象时,一切都变得容易了。一旦他们内联了代码,前进的道路就变得清晰,添加新功能也变得更快更容易。

The moral of this story? Don't get trapped by the sunk cost fallacy. If you find yourself passing parameters and adding conditional paths through shared code, the abstraction is incorrect. It may have been right to begin with, but that day has passed. Once an abstraction is proved wrong the best strategy is to re-introduce duplication and let it show you what's right. Although it occasionally makes sense to accumulate a few conditionals to gain insight into what's going on, you'll suffer less pain if you abandon the wrong abstraction sooner rather than later.
这个故事的寓意是什么?不要陷入沉没成本谬误。如果你发现自己正在通过共享代码传递参数并添加条件路径,那么这个抽象就是错误的。它一开始可能是正确的,但那个时候已经过去了。一旦一个抽象被证明是错误的,最好的策略就是重新引入重复的代码,让它告诉你什么是正确的。虽然偶尔积累一些条件判断以了解情况是有意义的,但如果你尽早放弃错误的抽象,你将少受痛苦。

When the abstraction is wrong, the fastest way forward is back. This is not retreat, it's advance in a better direction. Do it. You'll improve your own life, and the lives of all who follow.
当抽象错误时,前进最快的方式是后退。这不是撤退,而是朝着更好的方向前进。去做吧。你将改善自己的生活,以及所有追随者的生活。

News: 99 Bottles of OOP in JS, PHP, and Ruby!
新闻:99 个面向对象编程的瓶子,支持 JS、PHP 和 Ruby!

The 2nd Edition of 99 Bottles of OOP has been released!
《99 个面向对象编程的瓶子》第二版已发布!

The 2nd Edition contains 3 new chapters and is about 50% longer than the 1st. Also, because 99 Bottles of OOP is about object-oriented design in general rather than any specific language, this time around we created separate books that are technically identical, but use different programming languages for the examples.
第二版包含 3 个新章节,篇幅约为第一版的 50%。此外,由于《99 个面向对象编程的瓶子》关注的是通用的面向对象设计,而非任何特定语言,因此这一次我们创建了技术上完全相同但使用不同编程语言作为示例的独立书籍。

99 Bottles of OOP is currently available in Ruby, JavaScript, and PHP versions, and beer and milk beverages. It's delivered in epub, kepub, mobi and pdf formats. This results in six different books and (3x2x4) 24 possible downloads; all unique, yet still the same. One purchase gives you rights to download any or all.
《99 Bottles of OOP》目前有 Ruby、JavaScript 和 PHP 版本,以及啤酒和牛奶饮品版本。它提供 epub、kepub、mobi 和 pdf 格式。这导致了六种不同的书籍和(3x2x4)24 种可能的下载;所有下载都是独一无二的,但内容相同。一次购买即可下载任何或所有版本。

Posted on January 20, 2016 .
发布于 2016 年 1 月 20 日,作者:Sandi Metz。