这篇文章翻译于React核心开发者Dan的这篇博客。
正文
那是一个深夜
我的同事刚刚提交了他们整个星期一直在编写的代码。我们正在开发一个图形编辑画布,他们实现了通过拖动矩形和椭圆等形状边缘的小手柄来调整其大小的功能。
代码能正常运行。
但显得很冗余。每个形状(如矩形或椭圆)拥有各自不同的手柄集合,而拖动手柄的不同方向会以不同的方式影响形状的位置和尺寸。如果用户按住 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);
代码的总大小减半,重复的部分也完全消失了!如此整洁。如果我们想要改变某个特定方向或形状的行为,我们可以在一个地方进行修改,而不是到处更新方法。
已经是深夜了(我太投入了)。我将我的重构代码提交到了主分支然后去睡觉了,为自己解开了同事混乱的代码而感到骄傲。
次日早晨
……并未如我所料。
上司找我进行了一次单独交谈,委婉地要求我撤销那次修改。我惊愕不已。旧代码一团糟,而我的代码整洁明了!
尽管心有不甘,我还是照做了。然而,我花了好几年才意识到他们是对的。
这只是一个阶段
对“清洁代码”痴迷、热衷于消除重复,是我们许多人必经的一个阶段。当我们对自己的代码缺乏信心时,往往会将自己的自我价值感和职业自豪感寄托于那些可度量的事物上。一套严格的代码风格规则、一种命名方案、一种文件结构、对重复的零容忍……
虽然无法完全自动化地消除重复,但随着练习,这一过程会变得愈发容易。通常情况下,每次修改后,你都能判断出代码中的重复是增多了还是减少了。因此,消除重复仿佛是在提升代码某个客观指标,给人以成就感。更糟糕的是,它还会影响人们的自我认知:“我是那种编写清洁代码的人”。这种错觉具有极强的迷惑性。
一旦我们掌握了创建抽象的能力,就很容易对此上瘾,只要看到重复的代码,就会迫不及待地从中抽离出抽象。经过几年编程历练,我们会发现到处都是重复——而抽象化正是我们的新超能力。如果有人告诉我们抽象是一种美德,我们会欣然接受,并开始评判他人不崇尚“清洁”。
我现在明白,那次所谓的“重构”在两方面都是一场灾难:
- 首先,我没有与原作者沟通。我在没有征得他们意见的情况下重写了代码并提交。即便这算是一种改进(我现在已不再这么认为),这种方式也极其糟糕。一个健康的工程团队始终在建立信任。未经讨论就擅自重写队友的代码,将严重损害你们在代码库上的协作效率。
- 其次,没有什么是免费的。我的代码牺牲了应对需求变更的能力,换取了减少重复,但这并非一笔划算的交易。例如,后来我们需要为不同形状的不同手柄添加许多特例和行为。若沿用我的抽象设计,实现这些变更会复杂数倍;而若是采用原先“杂乱”的版本,这些改动则轻而易举。
那我是否在建议你应该编写“脏”代码呢?并非如此。我想强调的是,当你谈论“清洁”或“脏乱”时,应当深入思考其含义。这些词语会让你产生反感、正义感、美感或是优雅感吗?你能否确切指出这些品质所对应的工程成果?它们又是如何具体影响代码的编写与修改方式?
我当初的确未曾深入思考这些问题,只是过分关注代码的外观,却忽视了它在一个由充满变数的人类组成的团队中如何演变。
编程是一段旅程。想一想从写下第一条代码至今,你已走了多远。第一次体验到通过提取函数或重构类让复杂代码变得简洁,想必令你欣喜不已。如果你对自己的技艺引以为豪,追求代码清洁自然颇具吸引力。那么,就先这样做一段时间吧。
但切勿止步于此。不要成为清洁代码的狂热信徒。清洁代码并非目标,而是我们在面对系统无尽复杂性时试图理清头绪的一种手段,是在面对未知领域、不清楚某项改动会对代码库产生何种影响时的导航工具。
让清洁代码指引你,然后适时放下它。