原文[1]:Dan Abramov[2] - 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 规则,一个命名方案,一个文件结构,没有重复代码。
你不能自动消除重复,但随着实践的增加,这确实会变得更容易。你通常可以看出每次更改后重复的部分是增加还是减少。因此,消除重复感觉就像是改善了代码的某种客观指标。更糟糕的是,它干扰了人们的身份认同感:_“我就是那种写整洁代码的人”_。这就像任何种类的自我欺骗一样有力。
一旦我们学会如何创建抽象[3],我们就会很容易对这种能力产生依赖,每当看到重复的代码,就会凭空提出抽象。编程几年后,我们看到重复无处不在——抽象是我们的新超能力。如果有人告诉我们抽象是一种美德,我们会全盘接受,甚至会开始评判其他人为什么不崇尚“整洁”。
我现在明白我的“重构”在两个方面都是灾难性的:
首先,我没有和写这段代码的人交谈。我重写了代码,没有他们的参与就提交了。即使这是一个改进(我现在不再这么认为),这也是一个糟糕的做法。一个健康的工程团队需要不断建立信任。在没有讨论的情况下重写你同事的代码,会严重打击你们在代码库上有效协作的能力。
其次,没有什么是免费的。我的代码以减少重复为代价,牺牲了改变需求的能力,这是不值得的。例如,我们后来需要为不同形状的不同手柄添加许多特殊情况和行为。我的抽象需要变得更加复杂才能实现这些,而在原始的“混乱”版本中,这样的更改则易如反掌。
我是在说应该写“脏”代码吗?不是。我建议深入思考你说“整洁”或“脏”时的含义。有一种反感的感觉吗?正义感?美感?优雅感?你有多确定可以列出对应于这些品质的具体工程结果?它们如何确切地影响代码的编写和修改[4]?
我肯定没有深入思考过这些问题。我考虑了很多关于代码看起来如何 —— 但并没有考虑它如何在一个由复杂多变的人组成的团队中发展。
编程是一场旅程。想想你从编写第一行代码到现在走过的路程。我想,第一次看到提取函数或重构类可以让复杂的代码变得简单,一定是一种快乐的体验。如果你对自己的技术感到自豪,那么就会很容易追求代码的整洁性。那就去追求吧。
但不要止步于此。不要成为一个整洁代码的狂热者。整洁的代码不是终极目标,它是我们试图理解我们所处理的巨大复杂系统的一种尝试。当你还不确定一个更改会如何影响代码库,但你在未知的混沌中需要指引时,它是一种防御机制。
让整洁的代码引导你,然后放手。
参考资料[1] 原文: https://overreacted.io/goodbye-clean-code/
[2] Dan Abramov: https://danabra.mov/
[3] 抽象: https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction
[4] 修改: https://overreacted.io/optimized-for-change/