代码改变世界

单一出口原则

2015-08-09 10:59  Peter87  阅读(5072)  评论(0编辑  收藏  举报

最近在读《重构——改善既有代码的设计》这本书,在 9.4 Remove Control Flag(移除控制标记)这一节,作者提到了“单一入口”和“单一出口”这两个原则,并对“单一出口”原则批驳了一番,让我想起了一个遥远的故事。

那是3年前在H3C实习的日子,开发部门对代码规范规定略微严格,并且有代码鉴定小组严格把关进行代码检查。尤其还记得当时对于“单一出口”原则的提倡,比如下面这段代码:

Function
{  
    if (condition)
    {       
        return false;
    }    
 
   return AnotherFunction();
}

会建议为如下的形式:

for loop
{         
   bRet = FALSE;
   if (!condition)
   { 
	bRet = AnotherFunction();
   }

   return bRet ;
}

在当时,我虽然觉得这样有道理,却实际上并没有明白其中的原因。但是这种做法有一个弊端,也就是如果一个函数当中条件较多时会使得该函数的嵌套层次暴增,这个时候代码阅读起来会比较费劲。最近在阅读编程实践相关书籍的时候,恰好碰到了对于该单一出口原则的不同的观点。

来自Boswell与Foucher的反驳

Boswell(Dustin Boswell)和Foucher(Trevor Foucher)是《The Art of Readable Code》一书的两位作者。在该书第7章的“Returning Early from a Function”小节当中有如下一段话:

Some coders believe that functions should never have multiple return statements. This is nonsense. Returning early from a function is perfectly fine—and often desirable.

很显然,作者认为刻板的遵守单一出口原则会影响到代码的可读性。同时,在“Minimize Nesting”一节,作者还举了一个使用“returning early from a function”来优化代码可读性的例子。

if (user_result == SUCCESS)
{
  if (permission_result != SUCCESS)
  {
    reply.WriteErrors("error reading permissions");
    reply.Done();
    return;
  }
  reply.WriteErrors("");
}
else
{
  reply.WriteErrors(user_result);
}
reply.Done();

优化后:

if (user_result != SUCCESS)
{
  reply.WriteErrors(user_result);
  reply.Done();
  return;
}

if (permission_result != SUCCESS)
{
  reply.WriteErrors(permission_result);
  reply.Done();
  return;
}

reply.WriteErrors("");
reply.Done();

这个例子主要提倡“在一些罕见情况发生时提前退出”,并没有直接针对单一出口原则。但也间接地反击了单一出口原则,因为单一出口原则是绝对不会支持提前return的。其实,我们可以将上面的例子简单修改一下,使其符合单一出口原则:

if (user_result == SUCCESS)
{
  if (permission_result != SUCCESS)
  {
    reply.WriteErrors("error reading permissions");
  }
  else // 简单的加上一个else.
  {
    reply.WriteErrors("");
   }
} else
{
  reply.WriteErrors(user_result);
}
reply.Done();

这就是就是对单一出口原则的一次反驳,看起来也不无道理。值得赞赏的是作者这里不仅解释了他们推崇方法的好处,也提到了为什么单一出口原则得以被提出的原因:保持单一出口原则的一个主要原因是在函数的结尾可以做统一的清理工作,特别对于C语言当中更应该如此。但是,对于现代编程语言当中本身已经提供了类似的机制。

	|| Language     ||  Structured idiom for cleanup code ||
	--------------------------------------------------------
	|| C++          ||           destructors              ||
	|| Java, Python ||           try finally              ||
	|| Python       ||              with                  ||
	|| C#           ||              using                 ||

插个小话题:对于作者提到的这个例子,我觉得还可以使用《重构》一书当中的“Consolidate Duplicate Conditional Fragments”进一步优化,如下:

if (user_result != SUCCESS)
{
  reply.WriteErrors(user_result);
  reply.Done();
  return;
}

if (permission_result != SUCCESS)
{
  reply.WriteErrors("error reading permissions");
}
else
{
  reply.WriteErrors("");
}

reply.Done();
return;

有关“单一出口原则”的更多内容

这里有一篇文章讨论了对该原则有更多讨论,多数人赞同应用该原则的一些例外情况:

  • GuardClause,所谓的“卫语句”,指的是在函数开头用来判断一些明显非法情况并立即退出的语句。比如常见的指针判空操作;
  • 函数当中存在有多种返回情况,比如switch/case语句当中的每一个分支执行之后均直接return;

但对该篇文章对应的另一篇文章却大费笔墨支持单一出口原则,尤其在诸如BASIC、Fortran、C等面向过程的编程语言当中:

I believe a single code block should have one point of entry and one point of exit. A function is a code block, so this holds true for functions. However it also and perhaps more importantly holds true for any other situation where it might be an issue. In languages like BASIC, Fortran, C and Assembly language you can create blocks of code that have multiple points of both entry and exit. Code where this happens is often called 'spaghetti code'. The more points of entry and exit the more difficult it is to debug the code. Eventually, it becomes impossible for any practical purpose.

另外在诸如C++这些现代化编程语言当中也是应该谨慎的:

More modern languages such as C++ offer facilities that insulate programmers from some of the consequences of breaking the single point rule. They do not cause it to stop being a sound rule. They do not turn poor practice into good practice. If you can think of a way that it can go wrong, it will go wrong in the fullness of time. The bizarre rationale offered for dispensing with this rule is born of limited experience and hubris.

这里这里和Overflow上面的讨论1讨论2有更多与此相关的内容,不妨移步去凑凑热闹。

自己的观点

在写这篇笔记的前后,我的观点是有一些细微的变化的。

从懵懵懂懂继承单一出口原则编程手法之后到不久之前,自己一直如此实践,也觉得这样写是一种自然的写法。当然,这主要得益于没有编写过逻辑太过庞杂的函数以至于让if...else语句嵌套出富丽堂皇的N层楼阁。所以,在阅读到《编写可读代码的艺术》和《重构——改善既有代码的设计》的相关章节之时,突然觉得自己原来一直所坚持的是错误的。这也促使自己决定将此记录下来,备忘之余说不定也能分享给其他人。

在阅读了不少网络上的观点之后,一颗激动的心反而冷静下来了不少。仔细想想,两种做法都具有可取之处,也不一定得非此即彼,一定要占到某个阵营当中去。比如,在笔记开头的那个例子当中可以不用遵守单一出口原则,代码阅读起来会更简洁。另外,不论是在面向过程,还是面向对象的程序设计语言之中,谨慎的遵守单一出口原则如果可以让程序的可读性更好,那就好好使用它。