阅读 199

[译文]解开嵌套代码

译者:白玉堂

作者:Jason McCreary

原文:jasonmccreary.me/articles/un…

我不相信写代码的硬性规则,但是你经常能听到。比如一个方法不应该超过15行,方法只应该有一个 return 语句,缩进必须是4个空格等等。这些规则太死板了。
实际代码要灵活的多。这些硬性规则带来的副作用是让我们偏离了实质 可读性。如果我的关注点完全放在行数或 return 语句,这只会阻碍自己写高可读性代码,因为它们有不少都“太长”或多个 return 语句。
很多这些硬性规则试图解决嵌套代码问题。嵌套代码很难跟进逻辑去理解。感官上,这样更需要用眼睛扫描更多;心理上,每个嵌套层级都需要付出更多精力跟踪理解功能,这些都会让阅读者费劲。
嵌套代码主要是条件判断的结果。自从条件语句成为编程逻辑的基础,我们不能很好的移除它们。我们必须识别出他们对读者的影响,采取措施减少这种影响。

回到顶部

为了提高可读性,我们想让代码回到顶部。循环和条件语句天生就有一个嵌套结构,在这些代码块中无法避免的要嵌套。然而,我们可以避免在此结构之外的嵌套。
我们一起看几个嵌套代码的例子来练习一下提高他们的可读性。

空代码块

可能你不相信我,但是我不止一次的看到这样的代码:

<?php

public function handle($request, Closure $next)
{
    if (env('APP_ENV') == 'development') {
        // do nothing...
    } else {
        if ($request->getScheme() != 'https') {
            URL::forceScheme('https');
            return redirect('https://www.example.com/' . $request->getPath());
        }
    }

    return $next($request);
}
复制代码

就是这,一个空的 if 语句块,我还看到过另一面:一个空的 else 代码块。没有规定一个 if 必须和一个 else 成对出现。至少在过去的 20 多年没有任何一门编程语言规定这样。空代码块是死代码,删除他们。

条件值

嵌套代码经常会返回一个值。如果值是 boolean 型,这是压缩代码的机会,直接 return 掉条件判断。
考虑一下这个 Set 类中 isEmpty 方法的嵌套代码:

<?php

public function isEmpty() {
    if ($this->size === 0) {
        return true;
    } else {
        return false;
    }
}
复制代码

尽管这个方法块只有 4 行代码,还包括多个子块。即使这么少的代码行数,也难于阅读,因为它让代码比它实际要出现更高的复杂度。
通过识别原始 boolean 值的条件返回,我们有很好的机会通过直接返回条件完全删除嵌套代码。

<?php

public function isEmpty() {
    return $this->size === 0;
}
复制代码

结合这个恰当的方法命名和现在的一行代码块的上下文,我们降低了代码的理解复杂度。尽管这行代码可能会比较密集,但它仍然比重构前更具可读性。
注:压缩条件判断可以适用于很多数据类型不仅限于布尔类型。例如,你可以使用简单类型的整型作为条件返回。然后,这也会迅速的增加了复杂度。很多的编程者尝试使用三目运算来解决这种情况。但是三目运算能够节省代码却没有降低复杂度,还会降低代码可读性。在这种情况下,卫语句是更好的选择。

卫语句

嵌套代码经常是逻辑层次递进的结果。我们作为编程者,要写出每一个条件直到可以安全操作的程度。
但是这个流程对程序执行来说是典范,对代码阅读却不是。因为每一层代码嵌套,代码阅读者必须保持一种持续增加的思维模式。
细想下面的 Setadd 方法的实现:

<?php

public function add($item) {
    if ($item !== null) {
        if (!$this->contains($item)) {
            $this->items[] = $item;
        }
    }
}
复制代码

代码逻辑是这样:如果item 不是 null 并且 如果 Set 类不包含 item,就把 item 添加到数组里。问题是:这不仅这么简单的操作让人感到复杂,更让主要操作埋藏在最深层的代码里。
理想状态下,代码的主要操作位于顶部。我们可以把条件语句重构成卫语句,达到解开嵌套语句和突出主要操作的目的。
卫语句只是想保护我们的方法不受一场路径的干扰。尽管他们通常初夏你在代码块的顶部,他们也可以出现在任何地方。我们可以运用 De Morgan's Laws 把任何的嵌套条件转换成卫语句并放弃层层的控制。代码里,这意味着我们不用条件语句,并采用 return语句。
把以上思路应用到  add 方法,我们的实现如下:

<?php

public function add($item) {
  if ($item === null || $this->contains($item)) {
      return;
  }

  $this->items[] = $item;
}
复制代码

译者注:其实卫语句也运用了《重构》里经常提到的尽早返回的思想,把异常情况直接打回去
这么做,我们不仅提炼了主逻辑,还突显出方法的异常路径。现在对未来的代码阅读者少了一些复杂度,这也让被突显出的异常情况更易于测试。

“切换”到 if

switch 语句是一个非常啰嗦的语法结构,switch 有固定的 4 个关键字和三个级别。即使代码块里只有几行代码仍然要阅读很多代码。虽然者在某些情况下可以接受,但在其他情况下并不是。
有的情况使用 if 语句来替代 switch 语句可能会产生更多较高可读性代码。

  • 当只有几个 case 代码块时,switch 语句的固定结构却比等效的 if 语句产生更多行代码。
  • 当 case 代码块包含嵌套增加了复杂度降低了可读性到达危险的水平,使用卫语句或大代码块的实践方法能够改进代码。
  • 当类型转换或计算需要引导值到 swtich 约束时,这不适用于不支持复杂的 case 比较的编程语言(如:swift,go 等)

switch 语句非常适用于 1 :1 的已有 case 语句并且有他们自己多行的代码块场景。无论这些代码行是赋值,return 语句或者方法调用,比率大致为 1 :1 则可读性几乎保持不变。

<?php

switch ($command) {
    case 'action':
        startRecording();
        break;
    case 'cut':
        stopRecording();
        break;
    case 'lights':
        adjustLighting();
        break;
}
复制代码

注:当 switch 语句是流线型的情况下,很多编程者使用字典/数据表,或多态代替。所有这些却是其他的替代品。就记住每一个方案都有权衡(复杂性),对于大多数代码,switch 语句通常“足够好”。

复杂的循环

另一个嵌套的表现是循环,循环具有天然的复杂度。作为一个编程者,我们就像被一个人诅咒了一样,总想...增量逻辑。同样,作为人类,我们不是计算机,我们不太可能赢得了和计算机的循环计算。计算机会一直比人类强大,能和复杂唯一与之抗衡的是可读性。
我不会介绍可能会改进你代码的数据结构或算法。那比较有特定性。通常,大多数循环处理累加或调用。如果你发现你的代码库包含太多循环,看下是否有类似 filter / map / reduce 等的高阶函数被使用。虽然这可能无法帮助所有的阅读代码者改进可读性,但它会提高你的个人技能。

译者后记

我常常在想,我们为什么要和代码做斗争?
1、代码没有错,代码构建的软件帮助公司创造价值,争取市场份额,创造了收益。
2、软件随着功能和需求的迭代,原始的架构设计肯定不能满足后续的变更,结果造成代码臃肿,维护成本增加,软件风险增加,也间接加剧了产品的风险,甚至对公司营收都会造成风险和影响。
3、软件的腐烂不可避免,我们能做的是延缓这种腐烂,不断重构我们的代码,重构不一定是整个接口或功能全部推倒重来,也可以是一个函数的优化,一行代码的优化。这些小的动作都能降低代码腐烂的速度,降低bug数,降低维护成本和扩展成本。于公,可以为公司节省开支;与私,开发也能早点下班;于职业规划,好的代码也是一个程序员的门面,对自己的代码质量和编程习惯负责也是以后求职的一个竞争优势。

基于原作者的分享,我也分享我看到的,在代码编写初期就能改进的,(如果代码编写完成了,再去重构,代价会更大),这些小动作就和“勿以善小而不为”一个道理,积少成多,我们每天都能写出比昨天更好的代码。

多余的变量

<?php
class Do
{
  	public $api_url = 'http://www.domain.com/api/name/action';
  
        public function requestRemoteApi(array $params)
        {
            $url = $this->api_url;
           // ... do someting
          
          $res =  HttpHelper::post($url,$data);
          
          return $res;
        }
}
复制代码

比如这里有两点:

  1. 类已经有一个全局变量 $api_url,在函数内部又重新赋值给一个局部变量,这个是完全没必要的。
  2. 响应如数据赋值给变量 $res,却没有做任何处理,再后边才返回这也是没有必要的,除非你要拿到结果做判断和处理,否则单独赋值给一个新的变量是多余的。

直接写

<?php
class Do
{
  	public $api_url = 'http://www.domain.com/api/name/action';
  
        public function requestRemoteApi(array $params)
        {
		
           // ... do someting
          
        		return  HttpHelper::post($this->api_url,$data);
        }
}
复制代码

直接的复制粘贴

在代码优化的原则里的确是有一个“延迟决定”的思想,就是说当我们做代码抽象的时候,一次是特例,两次是偶然,三次你就需要考虑抽象封装了。
但是在代码里我们往往看到完完全全的复制,只修改了一些简单的一行配置或者 变量名不同,更有甚者惧于生产环境风险直接复制了一个新的文件,甚至一个包含一千多个文件的文件夹,命名后缀只是简单的加了数字1/2/3/4 等等 folderName1 , folderName2 , folderName3 。

  • 我们始终要记得:代码是给人看的,并且不只是写给自己,是写给自己小伙伴。
  • 写好代码,写漂亮代码,是对自己的负责,对同事和团队的负责,更是对公司的负责。
  • 重构越早,代价越小

扩展阅读

软件的高质量意味着高成本?
技术债

关注下面的标签,发现更多相似文章
评论