再见,整洁代码
January 11, 2020
那是一个深夜。
我的同事刚刚提交了他们一周以来一直在编写的代码。我们正在开发一个图形编辑器画布,他们实现了通过拖动形状边缘的小控件来调整矩形和椭圆等形状大小的功能。
代码能正常运行。
但它非常重复。每种形状(比如矩形或椭圆)都有不同的控件,拖动每个控件的方向不同,会以不同的方式影响形状的位置和大小。如果用户按住 Shift 键,调整大小时还需要保持比例。这里面有一堆数学计算。
代码大致如下:
let Rectangle = {
resizeTopLeft(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeTopRight(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeBottomLeft(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeBottomRight(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
};
let Oval = {
resizeLeft(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeRight(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeTop(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeBottom(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
};
let Header = {
resizeLeft(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeRight(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
}
let TextBlock = {
resizeTopLeft(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeTopRight(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeBottomLeft(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
resizeBottomRight(position, size, preserveAspect, dx, dy) {
// 10 行重复的数学计算
},
};
这些重复的数学计算让我很难受。
它不够整洁。
大部分重复出现在相似的方向之间。例如,Oval.resizeLeft()
和 Header.resizeLeft()
有很多相似之处,因为它们都处理左侧控件的拖动。
另一种重复则出现在同一形状的方法之间。例如,Oval.resizeLeft()
和其他 Oval
方法也有相似之处,因为它们都处理椭圆。Rectangle
、Header
和 TextBlock
之间也有重复,因为文本块本质上也是矩形。
我有了一个想法。
我们可以通过如下方式分组代码,消除所有重复:
let Directions = {
top(...) {
// 5 行独特的数学计算
},
left(...) {
// 5 行独特的数学计算
},
bottom(...) {
// 5 行独特的数学计算
},
right(...) {
// 5 行独特的数学计算
},
};
let Shapes = {
Oval(...) {
// 5 行独特的数学计算
},
Rectangle(...) {
// 5 行独特的数学计算
},
}
然后组合它们的行为:
let {top, bottom, left, right} = Directions;
function createHandle(directions) {
// 20 行代码
}
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 行代码
}
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 规则、命名规范、文件结构、没有重复。
你无法自动化消除重复,但确实可以通过练习变得更容易。通常每次更改后,你都能判断出重复是变多还是变少。因此,消除重复让人感觉像是在提升某种客观的代码指标。更糟糕的是,这会影响人的身份认同感:“我是那种写整洁代码的人”。这和任何自我欺骗一样有力量。
一旦我们学会了如何做抽象,就很容易沉迷于这种能力,看到重复代码就想凭空抽象出一套方案。编程几年后,你会发现到处都是重复——而抽象成了你的新超能力。如果有人告诉你抽象是一种美德,你会欣然接受。然后你开始评判那些不崇尚“整洁”的人。
现在我明白了,我的“重构”其实在两个方面都是灾难:
-
首先,我没有和写这段代码的人沟通。我直接重写了代码并提交,没有征求他们的意见。即使这真的是一种改进(现在我并不这么认为),这种做法也很糟糕。一个健康的工程团队需要不断建立信任。不经讨论就重写队友的代码,会极大损害你们在同一个代码库中协作的能力。
-
其次,天下没有免费的午餐。我的代码用减少重复换来了需求变更能力的下降,这并不是一个好的交换。例如,后来我们需要针对不同形状的不同控件实现许多特殊情况和行为。我的抽象方案要支持这些变化就会变得极其复杂,而原本“乱糟糟”的版本却能轻松应对这些变更。
我是不是在说你应该写“脏”代码?不是。我建议你在说“整洁”或“脏乱”时,认真思考你真正的含义。你会有反感、正义感、美感、优雅感吗?你能否明确说出这些特质对应的具体工程结果?它们究竟如何影响代码的编写和修改?
我当时并没有认真思考这些。我只关心代码看起来如何——却没有考虑它在一群有血有肉的人协作下会如何演化。
编程是一段旅程。想想你从写下第一行代码到现在走了多远。我相信你第一次发现提取函数或重构类能让复杂代码变得简单时,一定很开心。如果你为自己的技艺感到自豪,很容易追求代码的整洁。尽情去做一阵子吧。
但不要止步于此。不要成为整洁代码的狂热分子。整洁代码不是目标。它只是我们试图在系统极度复杂时理清头绪的一种方式。当你还不确定某个更改会如何影响代码库时,整洁代码是一种防御机制,为你在未知的海洋中提供指引。
让整洁代码指引你。然后学会放下它。
Pay what you like