8

Goodbye Clean Code,这是对代码编写与重构的新感悟

 4 years ago
source link: https://www.jiqizhixin.com/articles/2020-01-19-5
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

AreMNbB.jpg!web

Clean Code,顾名思义就是整洁的代码,或者说清晰、漂亮的代码,相信大多数开发者都希望自己能写出这种类型的代码。

那么我们为什么需要 Clean Code 呢?这里需要明确的是,写代码并不只是用来跑一跑或实现某些功能,写代码更重要的是便于维护。所以从这一点出发,写代码就像写一篇文章,阅读体验应该非常流程,逻辑承接应该非常顺滑。

现在有两种文章会让我们阅读体验不难么好,一种是过度冗余,有一些简单的概念经常出现,让我们会觉得文章不够精炼。另一种是太精炼了,每一部分之间的逻辑跳跃比较大,或者说我们很难跟上作者的叙述逻辑,那么这样的文章看起来会显得生涩难懂。

代码也一样,有些重复使用的方法可以编入相同的函数,同类函数之间的关系可以编入类与对象。这样代码整体能显得更加「干净」。但需要注意的是,并不是说最紧凑的代码就是最好的,例如类的继承,如果说读懂当前类需要往上翻好几个类,这种体验并不友好,似乎代码的逻辑跳跃让人很难跟着走。

所以 Clean Code 到底好不好?

baI7Nfa.png!web

根据列表推导式(first method)精简代码(second method)。

React 主要维护者 Dan Abramov 也在考虑这件事,其中 React 是 Facebook 维护的 JavaScript UI 库。他在博客上写了一篇对 Clean Code 的反思,这篇文章在 HackNews 上获得了非常热烈的反响。下面我们具体看看 Dan Abramov 眼中的代码编写准则。

精简的魅力

已经到了深夜,我的同事正在检查这一周写的所有代码。他们在做的东西可以理解为,通过拉拽图形边缘的小控件来变成矩形和椭圆形等形状,代码本身是没有问题的。

但似乎代码的重复性有点多,每一个形状都有一组不同的控件,从不同方向拉拽每一个控件都会以不同的方式影响形状的位置与大小。如果用户按住了 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
  },
};

我们可以看到上面已经是简化的代码了,每一条计算语句下都有 10 条几乎重复的计算过程。这样的重复性计算代码看起来就很冗余,它也不是一种 Clean Code。

我们很容易发现,大多数重复代码都有相似的属性。例如 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);

上面这种方法只会有一半的代码量,重复代码基本都被删除了,所以它是干净的。如果我们想改变图形方向或形状等特定属性,我们只需要修改一段代码,而不需要到处更新这个函数。

现在代码过于重复这个问题解决了,我们可以开心地把它提交给代码库。并且因为写了更简洁的代码,我们可以带着成就感上床睡觉了。

事情并不那么简单

但是等等,到了第二天,你会发现事情并不简单。可能老板会找你谈话,委婉地想要你撤回昨晚重构的干净代码。但这出现了什么问题?重构的代码非常简单,它比之前一堆乱麻的代码漂亮很多。

当时你很可能心不甘情不愿,但几年后,你会发现老板的观点才是正确的。

痴迷于「Clean Code」并删除重复代码是我们都会经历的一个过程。当我们对代码感到不太自信时,我们很容易将自我价值与自信感联系到一些可衡量的标准。例如一组严格的代码规则、一个确定的命名策略、一个明确的文件结构和没有重复的「干净」代码等。

在实践中,我们很自然地想着删除重复代码。我们通常知道每一次修改代码后的长短变化,因此移除重复代码可以提升一些客观的代码度量标准。不过糟糕的是,这种现象扰乱了我们的认同感:「虽然难懂一些,但我现在是在写一种干净的代码。」

一旦我们学会了创建 Abstraction,就很容易对这种能力产生很高的期望,并且每当我们看到重复代码就会想起一种「高效」的抽象方法。经过几年的代码经验后,我们一眼就可以看到各种重复代码,抽象就是我们新的能力。如果有人告诉我们抽象是一种美德,我们就会欣然接受它,同时也会因为别人不崇尚「清洁代码」而对他们品头论足。

现在,我们要考虑到,「重构」是一场灾难,它主要体现在两方面:

首先,我们并不能和写代码的人直接交流,我们只是重写代码,并简要地检查它。即使这是一种进步,那也是一种非常糟糕的方式。一个健康的工程团队需要不断建立信任,在没有讨论的情况下重写同事的代码是对协作的一个打击。

其次,什么都是有成本的,我们的代码会在后续的修改需求与降低重复性上进行权衡。还是之前的例子,如果我们需要为不同形状上的不同控件提供许多特殊能力。那么我们的抽象需要复杂好几倍才能完全囊括它们,而在最初「冗余」的代码中,添加新行为简直就是小菜一碟。

这难道意味着我们需要写「不干净」的代码?并不是的,我们需要仔细思考到底「干净」和「不干净」指的都是什么。

写代码就是一段旅程,我们需要考虑这段旅程到底需要走多远,也需要考虑我们现在的位置又在哪。如果我们第一次通过函数或重构一个类来令代码变得更简单,那么会获得很多满足感。如果我们对自己代码感到比较满意,那么追求更干净的代码是非常好的,我们可以在这个阶段持续做一段时间。

但不要止步于此,不要做一个干净代码的狂热推崇者。干净的代码并不是最终目标,只是我们在处理复杂系统的一个尝试。我们可能并不知道这种修改最终对代码库有什么样的影响,但是干净的代码会指引一条明路,至少这个方向是对的。

干净代码可以指引我们,但熟悉后应该放松它的指引。代码库整体的逻辑与风格,整体的可读性与修改便捷性,才是我们该追求的。

参考链接:

https://overreacted.io/goodbye-clean-code/

https://news.ycombinator.com/item?id=22022466

https://zhuanlan.zhihu.com/p/25541626

文为机器之心报道, 转载请联系本公众号获得授权

✄------------------------------------------------

加入机器之心(全职记者 / 实习生): [email protected]

投稿或寻求报道:content @jiqizhixin.com


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK