原文:Dan Abramov - 2020.01.11
那是一个深夜。
我的同事刚刚提交了他们一周编写的代码。我们正在开发一个图形编辑器的画布,他们实现了通过拖动边缘的小手柄,来调整形状(如矩形和椭圆)的大小的功能。
代码是有效的。
但是,它有些重复。每种形状(如矩形或椭圆)都有一组不同的手柄,每个手柄在不同的方向上拖动,会以不同的方式影响形状的位置和大小。如果用户按住 Shift 键,我们还需要在调整大小的同时保持比例。这里涉及到一堆数学计算。
代码看起来像这样:
// 矩形
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},
};
这种重复的数学计算真的让我很困扰。
它并不整洁。
大部分的重复是在相似的方向之间。例如,Oval.resizeLeft()
与 Header.resizeLeft()
有相似之处。这是因为它们都处理了在左侧拖动手柄的情况。
另一种相似性是在同一形状的方法之间。例如,Oval.resizeLeft()
与其他 Oval
方法有相似之处。这是因为它们都处理了椭圆。在 Rectangle
、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},
}
然后组合它们的行为:
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);
代码的总量减半,重复的部分完全消失了!它是如此整洁。如果想改变某个方向或形状的行为,我们可以在一个地方进行修改,而不是在各处更新方法。
已经是深夜了(我有点过于投入)。我将重构代码提交到 master 分支,然后满怀自豪地上床睡觉,因为我解开了同事混乱的代码。
第二天早上
…并不像我预期的那样。
我的老板邀请我进行一对一的聊天,他礼貌地要求我撤销昨夜的更改。我感到震惊,旧的代码一团糟,而我的代码整洁!
我勉强同意了,但我花了好几年的时间才看出他们是对的。
这只是一个阶段
痴迷于“整洁的代码”和消除重复是我们许多人都会经历的阶段。当我们对自己的代码没有信心时,我们很容易将自我价值和职业骄傲寄托在一些可以衡量的东西上。一套严格的 lint 规则,一个命名方案,一个文件结构,没有重复代码。
你不能自动消除重复,但随着实践的增加,这确实会变得更容易。你通常可以看出每次更改后重复的部分是增加还是减少。因此,消除重复感觉就像是改善了代码的某种客观指标。更糟糕的是,它干扰了人们的身份认同感:“我就是那种写整洁代码的人”。这就像任何种类的自我欺骗一样有力。
一旦我们学会如何创建抽象,我们就会很容易对这种能力产生依赖,每当看到重复的代码,就会凭空提出抽象。编程几年后,我们看到重复无处不在——抽象是我们的新超能力。如果有人告诉我们抽象是一种美德,我们会全盘接受,甚至会开始评判其他人为什么不崇尚“整洁”。
我现在明白我的“重构”在两个方面都是灾难性的:
- 首先,我没有和写这段代码的人交谈。我重写了代码,没有他们的参与就提交了。即使这是一个改进(我现在不再这么认为),这也是一个糟糕的做法。一个健康的工程团队需要不断建立信任。在没有讨论的情况下重写你同事的代码,会严重打击你们在代码库上有效协作的能力。
- 其次,没有什么是免费的。我的代码以减少重复为代价,牺牲了改变需求的能力,这是不值得的。例如,我们后来需要为不同形状的不同手柄添加许多特殊情况和行为。我的抽象需要变得更加复杂才能实现这些,而在原始的“混乱”版本中,这样的更改则易如反掌。
我是在说应该写“脏”代码吗?不是。我建议深入思考你说“整洁”或“脏”时的含义。有一种反感的感觉吗?正义感?美感?优雅感?你有多确定可以列出对应于这些品质的具体工程结果?它们如何确切地影响代码的编写和修改?
我肯定没有深入思考过这些问题。我考虑了很多关于代码看起来如何 —— 但并没有考虑它如何在一个由复杂多变的人组成的团队中发展。
编程是一场旅程。想想你从编写第一行代码到现在走过的路程。我想,第一次看到提取函数或重构类可以让复杂的代码变得简单,一定是一种快乐的体验。如果你对自己的技术感到自豪,那么就会很容易追求代码的整洁性。那就去追求吧。
但不要止步于此。不要成为一个整洁代码的狂热者。整洁的代码不是终极目标,它是我们试图理解我们所处理的巨大复杂系统的一种尝试。当你还不确定一个更改会如何影响代码库,但你在未知的混沌中需要指引时,它是一种防御机制。
让整洁的代码引导你,然后放手。