通过增加函数来提高代码质量的 7 个理由

1 原文信息 

1.1 标题 

7 Ways More Methods Can Improve Your Program

1.2 地址 

http://henrikwarne.com/2013/08/31/7-ways-more-methods-can-improve-your-program/


2 提取函数的 7 个好处 


我写的很多代码都是由相对较短的函数组成,而不是很长的那种。大家知道,函数就是要为了实现某项功能而定义的。不过,这样有很大的提高空间。通过重构来产生更多函数,产生更好的程序结构,从而使得程序能够很容易地被理解、更容易地来修改、更容易地测试以及更容易地调试。下面是介绍为什么采用更多的函数是一个好的实现的 7 个理由。


2.1 隐藏详细信息 


我在最近的工作中,在一个输出“电话信息记录(CDRs)”的函数中使用了如下 Java 代码:


// Check if CDR should be excluded because of sink filter
if (profile.isEnableSinkFilter() && profile.getSinkFilter() != null) {
  String[] sinkFilters = profile.getSinkFilter().split(";");
  for (String str : sinkFilters) {
    if (str.equals(sinkName)) {
      sinkNameMatch = true;
      break;
    }
  }
 
  if (sinkNameMatch && profile.isInvertSinkFilter()) {
    return result; // Do not output anything
  }


  // 译注: 这里双重取反
  if (!sinkNameMatch && !profile.isInvertSinkFilter()) {
    return result; // Do not output anything
  }
}


这部分代码展示了通过过滤字段(sink name)来实现过滤功能。与是否存在匹配或这个过滤器按正序还是反序的条件来决定是否需要退出(通过返回结果语句),还是继续执行更多的处理。这段代码是没有问题的。它的的确确实现了它需要做的事情。问题在于,我每次读到这个函数时,不得不来认认真真地来坚持过滤过程是怎样实现的。但大部分情况下,我们仅仅只需要知道过滤操作已经做完了。详细的代码实现并不是最重要的。所以我把具体实现的代码单独提取到一个新的函数中: excludeBasedOnSinkName(),最终实现的代码如下所示:


if (excludeBasedOnSinkName(profile, sinkName)) {
  return result; // Do not output anything
}


这个函数的名字已经告诉我了它实现的功能,并且简单的函数使得阅读变得更加容易,因为实现过滤的细节都已经被隐藏在新实现的函数中了。如果我想了解具体实现的细节,我只需要跳转到这函数中就可以了解到了。实际上,大部分情况下,看到这个函数的名字就已经知道这块代码是实现什么样的功能了。


2.2 避免重复代码 


如果同样的代码在多处地方出现,那么,重复的它们应该被提取到一个新的函数中。这样做的话,会产生一些很好的效果。这样可以使得程序看起来更加的短小精炼。它能够确保在每一次情况下,所有的语句都得到执行、按照同样的代码顺序执行。还有,我最喜欢的一个原因是:在这段代码中有些地方需要修改的时候,仅仅只需要修改这一个函数就可以啦。假设你在 15 个不同的地方有这些重复的代码,那么,就很容易作出修改某处,但忘记修改了另外一处的情况。


不幸的是,出现重复代码的情况非常普遍。这通常是由于“复制-粘贴”操作造成的结果。拷贝相同的代码,修改了一小部分地方后,然后放到一个需要这样操作同等的消息处理函数中。“复制-粘贴”代码通常比创建一个拥有公共功能的函数(都是从一个地方被调用执行)更加快捷。但这需要指出的是,有些地方确实需要保留两份独立的代码的。对于这个,大家可能存在不同的观点。但在这种情况下,将公共部分提取到一个函数中,然后通过变量来传递参数来区分不同的情况将会是一个不错的选择。


Martin Fowler 在 IEEE 软件开发网站上写了一篇关于这个主题非常好的文章:避免重复(Avoiding Repetition)。


2.3 能分组相关行为的代码 


在某些情况下,一些事情是需要按照一个特定的顺序来完成的。通过创建一个函数然后调用它,你需要确保的是没有忘记调用某个步骤了。举个例子,如果你想停止一个正在运行的计时器,然后释放它,可以使用一个 stopTimer() 函数:


private void stopTimer() {
  if (this.timer != null) {
    this.timer.stop();
    this.timer = null;
  }
}


如果使用了函数来替代几条独立的语句,在这个函数被调用之后,你就已经知道这个定时器已经停止了,并且这个定时器变量已经被赋值为 null 了。但注意这是工作只是在概念上的。当你仅仅想做的只是停止定时器的话,那么就不是单单注释掉这停止定时器语句这么简单了。


2.4 创建关键执行点 


当有很多复杂的处理流程需要完成的时候,添加一个关键执行点的函数是非常有帮助的,因为不管其他地方有没有完成,我们知道这个关键点已经完成了。举个例子,当打开一个电话时,一个首先要做的步骤是检查账户是否有足够的钱来打这个电话。然后就可能接着启动不同的通话类型,同时在那些地方都能检查操作是否完成了。所以,通过在一个函数中来检查所有的情况,通过这个唯一的方法,所有不同的通话类型被称之为“funneled”。


关键点函数使得程序更加容易地理解。不管之前发生了什么,每一次通话启动之前都会调用这个函数。这也使得调试变得更加的方便。你能够在这些关键点函数中打印输出代码,或者它们可以给你一个正确确定的检查点。所以,这使得查看通话启动流程变得更加容易。


2.5 简化测试用例 


长长的函数通常做了很多事情,但这会使得测试变得更加困难。例如,在我们工作中,用一个序列号来限制允许同时通话的数量。同时,你能够配置在达到多少通话量时弹出一个警告弹出框。需要一个简单的过程来计算弹出警告框的通话量,给出序列号值以及百分比。通过把计算过程提取到一个单独的函数中,达到很方便地为这个函数定义测试用例的方式。当限制设定为0%时,是否会给出正确的通信量?那么设定限制为100%的情况呢?当被除的的变量域是一个小数的情况会是怎样呢?如果是通过一个非常长的函数实现了计算过程,那么就只能和其他的一些操作混合在一起测试,而这样会是测试变得更加困难。


有些情况(通常情况下由于逻辑代码通常是在单元测试之前写好的),来创建适合测试要求的代码会变得相对比较复杂。一个简单的解决方案是定义一个静态函数来处理所有输入参数。这个静态函数能够在复杂对象还没有创建的时候被调用。


2.6 简化日志输出和调试 


一个函数如果仅仅只包含了一行代码会不会太短啦?不会。一个像这样的函数是非常有用的:


private void setState(State newState) {
  this.state = newState;
}


在这几天的工作中,我扮演了程序故障分析的角色。同时,我也想检查当用户执行复位操作时,是不是所有事情都已经关闭了属性。不幸的是,这个状态变量(state)在将近 25 个地方被设成空闲状态了,所以这花费了一些的工作来检查所有的情况。如果早使用了 setState 函数的,我只需要添加如下一条语句就可以啦:


private void setState(State newState) {
  if (newState == State.IDLE && !everythingClosed()) {
    logDebug("Going IDLE, but everything not closed");
  }
  this.state = newState;
}


一个 setState 函数对于打印所以状态改变也是非常有帮助的:


private void setState(State newState) {
  if (logDebugEnabled()) {
    logDebug("Changing state from " + this.state +
      " to " + newState);
  }
  this.state = newState;
}


在查找实际代码发生了什么情况时,能够在日志中看到所有的状态转变信息是非常有帮助的。


2.7 代码本身就是文档 


代码通常被读的次数比写的次数多得多。那么,写一个易于理解的代码是非常重要的。良好命名的函数能够更容易地理解整个程序。同时,你不必写一大堆的注释,因为函数的名称(以及变量)已经告诉了你缘由。想出一个命名良好的名字通常不太容易,但在我认为,这是编程过程中一个必不可少的部分。通过我实践中,检查是否是个好名字的方法是,在几个月后回头看代码的时候(当时我几乎已经忘记所有代码的实现细节):我能不能仅仅看到函数的名称就知道这个函数要做什么,或者我是不是还需要跟进到函数中来仔细研读这些代码才知道这个函数的功能?


我几乎从来都不会为内部函数写任何的 Javadoc 。函数的名称已经足够了。得到收益的地方时更短的、更全面的的代码——我看到在整个屏幕中看到的都是代码。当然,如果名字并不能完整解释功能的时候,也能够很容易地跟进到函数中然后检查代码实现的功能。唯一的例外是 API 文档。那里的 Javadoc 是非常重要的,因为你没有了跟进函数检查代码功能的机会了。


2.8 为什么不呢? 


那么为什么还有如此多的代码没有通过提取函数来获益呢?我片面地认为很多代码都是通过长时间的不同人的添加修改。这在每一次单独的更新中看不到提取函数等重构操作的价值,所以没有人愿意来做重构。


我也觉得可能是因为你所使用工具的局限性引起的。在过去几年中,我通过 IntelliJ IDEA 开发环境来编写 Java 代码,这个 IDE 可以实现通过一系列的按键能够进入和跳出类和其中的方法。所以是很容易添加很多函数的。当我正通过使用 emacs 来编辑 C++ 代码的时候,我的函数倾向于变得很长,我编写的类倾向于变得庞大。我认为造成这个现象的原因是如果添加了很多函数,将会花费更多时间在切换文件和缓冲区的过程中,而且还必须使用查找功能通过函数名称来找到它。


也有另外一个原因是,一些程序员仅仅就是因为喜欢看到一大块的代码,而不是几个相对分离的小函数。当我第一次阅读代码的时候,我总是跟进到所有函数中,然后读懂它们实现的功能。但是在我实现了很好的函数命名之后我就不需要这样做了,而且每次我看到这个函数的名称时,不需要再跟进读函数的代码。函数的名称表达的意思就已经很足够了。我比较喜欢通过函数的名称来了解函数所实现的功能——这使得阅读代码变得更加快捷。


2.9 结论 


以上就是我对取函数来提高代码质量的分析过程。通常情况下,可能在做一次提取函数操作时就得到了几种好处。例如:分组行为相近的函数也会隐藏细节,避免重复代码也可以达到代码即文档的作用。需要经常判断当前代码中是否适合插入一个函数。也许你们可能有这样的疑问,多添加函数会不会做过头了?实际工作中,我看到出错的代码一般都是出现在长长的函数中。所以,下次你修改同样重复功能的代码时,看看能否通过提取一个函数,是否会让你感受到这样做的好处呢?


3 好词好句 


sink name 汇总名称
inverted 反向的,倒转的
enclosing 包括在内,封闭的
conceptual 观念的
funnels 漏斗
trouble shooting 故障分析
concurrent 同时发生的
over-done 过头了


Call Detail Records 通话信息记录


4 译者感悟 


其实,文章介绍的内容很像《重构》这本书中提到的重构原则“提取函数”,一般建议一个函数的代码不要超过 150 行。就把这篇文章当做复习重构原则的一种方式吧。


看来大神也都喜欢用 emacs,只不过,这里写的是编辑 c++ 代码函数时会变得很长了,看来用 emacs 编辑 c++ 代码确实还是不方便。是因为神一样的编辑器也是有局限性还是文章作者没有找到好的方法呢?读者自己来寻找答案吧~





你可能感兴趣的:(程序员,重构,软件开发)