【译】代码洁癖的误区

595 阅读9分钟

原文链接: overreacted.io/goodbye-cle…


Goodbye, Clean Code


It was a late evening.


My colleague has just checked in the code that they’ve been writing all week. We were working on a graphics editor canvas, and they implemented the ability to resize shapes like rectangles and ovals by dragging small handles at their edges.


一天晚上。


我的同事刚刚提交了这周的代码,我们在完成一个图形编辑器,他们实现了通过拖拽边缘的小手柄来调整矩形和椭圆形等图形的能力


The code worked.


But it was repetitive. Each shape (such as a rectangle or an oval) had a different set of handles, and dragging each handle in different directions affected the shape’s position and size in a different way. If the user held Shift, we’d also need to preserve proportions while resizing. There was a bunch of math.


代码正常运行。


但是有很多重复的地方。每个图形(例如矩形和椭圆形)有一组不同的手柄,沿着不同的方向每个手柄会以不同的方式影响图形的位置和大小。如果用户按住 shift 键,那么拖拽的过程中还要保持原有的比例。一大堆的数学问题。


The code looked something like this:

代码大致如下:


let Rectangle = {
  resizeTopLeft(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeTopRight(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeBottomLeft(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeBottomRight(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
};
let Oval = {
  resizeLeft(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeRight(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeTop(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeBottom(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
};
let Header = {
  resizeLeft(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeRight(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },  
}
let TextBlock = {
  resizeTopLeft(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeTopRight(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeBottomLeft(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
  resizeBottomRight(position, size, preserveAspect, dx, dy) {
    // 10 repetitive lines of math
  },
};


That repetitive math was really bothering me.


It wasn’t clean.


Most of the repetition was between similar directions. For example, Oval.resizeLeft() had similarities with Header.resizeLeft(). This was because they both dealt with dragging the handle on the left side.


The other similarity was between the methods for the same shape. For example, Oval.resizeLeft() had similarities with the other Oval methods. This was because they all dealt with ovals. There was also some duplication between Rectangle, Header, and TextBlock because text blocks were rectangles.


I had an idea.


We could remove all duplication by grouping the code like this instead:


这一堆的重复数学代码很很困扰我。


他根本不够简洁。


在相似的方向上会有很多重复的代码。像椭圆形的左侧调整和椭圆形的其他方法很像。这是因为都在处理椭圆形。

因为 text blocks 也是矩形的,所以在矩形,Header 和 TextBlock 也是有很多重复代码。


我有个想法。


我们可以通过分组将重复的代码代替掉,像下面一样:


let Directions = {
  top(...) {
    // 5 unique lines of math
  },
  left(...) {
    // 5 unique lines of math
  },
  bottom(...) {
    // 5 unique lines of math
  },
  right(...) {
    // 5 unique lines of math
  },
};
let Shapes = {
  Oval(...) {
    // 5 unique lines of math
  },
  Rectangle(...) {
    // 5 unique lines of math
  },
}


and then composing their behaviors:


然后组装他们的行为:


let {top, bottom, left, right} = Directions;
function createHandle(directions) {
  // 20 lines of code
}
let fourCorners = [
  createHandle([top, left]),
  createHandle([top, right]),
  createHandle([bottom, left]),
  createHandle([bottom, right]),
];
let fourSides = [
  createHandle([top]),
  createHandle([left]),
  createHandle([right]),
  createHandle([bottom]),
];
let twoSides = [
  createHandle([left]),
  createHandle([right]),
];
function createBox(shape, handles) {
  // 20 lines of code
}
let Rectangle = createBox(Shapes.Rectangle, fourCorners);
let Oval = createBox(Shapes.Oval, fourSides);
let Header = createBox(Shapes.Rectangle, twoSides);
let TextBox = createBox(Shapes.Rectangle, fourCorners);


The code is half the total size, and the duplication is gone completely! So clean. If we want to change the behavior for a particular direction or a shape, we could do it in a single place instead of updating methods all over the place.


It was already late at night (I got carried away). I checked in my refactoring to master and went to bed, proud of how I untangled my colleague’s messy code.


这样代码少了一半的大小,并且重复的代码也没了!非常干净的代码。如果我们想去改变特定方向或者形状的行为,我们可以只改一个地方就行了,不用到处修改代码了。


已经是深夜了(我有点忘乎所以)。我向 master 分支提交了我的重构代码,然后,上床睡觉,为自己解决了同事的恶心代码而骄傲。


The Next Morning

… did not go as expected.


My boss invited me for a one-on-one chat where they politely asked me to revert my change. I was aghast. The old code was a mess, and mine was clean!


I begrudgingly complied, but it took me years to see they were right.


第二天早上


事情不像预期的那样,我的老板婉转地约我单独聊一聊。并要求我 revert 我的重构代码。我被震惊了。旧代码是杂乱的,老子的代码是完美的!


虽然我但是很不情愿,但是很多年后我理解他们是对的。


It’s a Phase

Obsessing with “clean code” and removing duplication is a phase many of us go through. When we don’t feel confident in our code, it is tempting to attach our sense of self-worth and professional pride to something that can be measured. A set of strict lint rules, a naming schema, a file structure, a lack of duplication.


这只是一个阶段


痴迷于干净的代码并且消除重复是我们很多人必经的一个阶段。但我们对自己的代码不够自信时,我们很容易将我们的自我价值和职业自豪感用一些可以衡量的东西联系起来

。像是一套严格的 lint 规则,一个命名模式,一个文件结构,没有重复和冗余等。


You can’t automate removing duplication, but it does get easier with practice. You can usually tell whether there’s less or more of it after every change. As a result, removing duplication feels like improving some objective metric about the code. Worse, it messes with people’s sense of identity: “I’m the kind of person who writes clean code”. It’s as powerful as any sort of self-deception.


你不能自动的删除重复,但是可以通过一系列的练习可以容易的干掉重复的代码。你通常可以判断出你的修改是变少了还是增加了代码。因此,消除重复的代码就像改进了代码的一些客观度量值。更糟的是,它会让人差生误解:“我是要写干净代码的人”。 一种强大的自欺欺人的认同感。


Once we learn how to create abstractions, it is tempting to get high on that ability, and pull abstractions out of thin air whenever we see repetitive code. After a few years of coding, we see repetition everywhere — and abstracting is our new superpower. If someone tells us that abstraction is a virtue, we’ll eat it. And we’ll start judging other people for not worshipping “cleanliness”.


一旦我们学会了创建抽象 ,很容易让人跃跃欲试地去尝试,并且当我们看到重复的代码时,很容易就去凭空去抽象它。经过几年的编程后,我们觉得到处都是重复的代码——而抽象就是我们新的超能力。我们会认可抽象就像是一种美德的说法。然后去鄙视那些不崇拜代码干净的人。


I see now that my “refactoring” was a disaster in two ways:


Firstly, I didn’t talk to the person who wrote it. I rewrote the code and checked it in without their input. Even if it was an improvement (which I don’t believe anymore), this is a terrible way to go about it. A healthy engineering team is constantly building trust. Rewriting your teammate’s code without a discussion is a huge blow to your ability to effectively collaborate on a codebase together.


现在我明白了,我的重构在以下两个方面是不好的:


第一,我没有和写代码的人交流过,我重写并提交的过程中没有任何其他人的输入。即使我的重写是一种更好的形式(我现在不觉得是),这是一种糟糕的方式。一个健康的工程团队是相互信任的。不经过讨论就重写你队友的代码对你在代码库上的有效协作能力是巨大的打击。


Secondly, nothing is free. My code traded the ability to change requirements for reduced duplication, and it was not a good trade. For example, we later needed many special cases and behaviors for different handles on different shapes. My abstraction would have to become several times more convoluted to afford that, whereas with the original “messy” version such changes stayed easy as cake.


第二,没什么是不需要代价的。我的代码依赖现有需求来减少重复,是不够好的处理。例如,如果之后我们要为不同的图形的不同手柄处理特殊的逻辑和行为。我的抽象就捉襟见肘了,他需要复杂很多倍才能处理。而用最初的杂乱版本处理就像切蛋糕一样容易。


Am I saying that you should write “dirty” code? No. I suggest to think deeply about what you mean when you say “clean” or “dirty”. Do you get a feeling of revolt? Righteousness? Beauty? Elegance? How sure are you that you can name the concrete engineering outcomes corresponding to those qualities? How exactly do they affect the way the code is written and modified?


那就应该写杂乱的重复代码了吗? 不,我建议仔细想一想你所以为的 “干净”和 “脏乱”的含义。是不满?正直?漂亮还是优雅?你如何确定你能说出与这些品质相匹配的具体的工程结果是什么样子的呢?他们到底是如何影响代码的编写和修改方式


I sure didn’t think deeply about any of those things. I thought a lot about how the code looked — but not about how it evolved with a team of squishy humans.


我的确没有认真思考过这些事,我一味追求了代码应该是怎么样的,却没有思考过他在团队中如何演进的过程。


Coding is a journey. Think how far you came from your first line of code to where you are now. I reckon it was a joy to see for the first time how extracting a function or refactoring a class can make convoluted code simple. If you find pride in your craft, it is tempting to pursue cleanliness in code. Do it for a while.


编程就像是一次旅行一样。考虑你现在的代码与第一行代码的对比。我认为,像第一次提取函数或是重构一个类让代码更简单的那种乐趣一样。如果你对自己专业的技术感到自豪,那么追求代码的干净是很具有诱惑力的,放手去做吧。


But don’t stop there. Don’t be a clean code zealot. Clean code is not a goal. It’s an attempt to make some sense out of the immense complexity of systems we’re dealing with. It’s a defense mechanism when you’re not yet sure how a change would affect the codebase but you need guidance in a sea of unknowns.


但不要止步于此,不要盲目地追求代码的干净。干净的代码不是目标。他只是我们在试图解决系统复杂性中的一种积极的尝试。当你还不确定更改会如何改变代码库时,但是需要在未知的领域中获得指导时,这是一种防御机制。


Let clean code guide you. Then let it go.


让干净的代码指导你,还是算了吧。