面条代码 vs. 馄沌代码

来源:互联网 发布:国家应该取缔网络直播 编辑:程序博客网 时间:2024/04/28 02:33

面条代码 (Spaghetti Code) 指的是冗长,控制结构复杂,混乱而难以理解的代码。

就我个人而言,曾经编写过大量面向对象和面向过程的代码,也曾经写过至少数千行的函数式代码,印象中,从来没有编写过冗长复杂的函数。有趣的是,我从来没有把短小精干,低圈复杂度的函数当做一个目标(直到2007年我才第一次听说“圈复杂度”这个名词,直到现在也讲不清楚它的计算方法),它只是消除重复分离关注点清晰表达等价值观驱动下的一个结果。

在从事咨询工作以后,我见到了大量的“面条代码”,更准确的说,见到的大多是“面条代码”。于是开始对自己的能力感到怀疑,并对那些有能力和“面条代码”和平共处的工程师感到钦佩。

因为,从骨子里我就对复杂事物感到恐惧和缺乏耐心。我喜欢一目了然,逻辑清晰的事物,如果一个设计可以让我舒服的靠在凳子上,无须看代码,仅凭思考就能在脑海中浮现清晰的画面,我才会感到踏实——everything is under control。

我相信,每个人都会喜欢这种踏实感。由此可以推论出,那些能够编写和控制“面条代码”的工程师,一定都具备优秀的理解力和控制复杂事物的能力。

直到有一天,我在《反模式》一书中看到了对它的定义和描述,才知道原来有这么多的人痛恨它,看来像我一样能力低下的人并不在少数。随后看到Uncle Bob在《Clean Code》中谈到方法长度和类规模时,均提到“第一条规则是短小,第二条规则是还要更短小”。至此,我彻底感到释然——能和大师一样愚笨,就没什么可感到羞耻的了。


平均复杂度


按照短板理论,一个团队所编写的代码,应该让团队中最笨的人也可以容易理解,这样团队的整体生产效率才能有效提升。当然你也可以把最笨的人踢出团队,但剩下的成员里,相对于最聪明的人,依然会存在最笨的人。

不信?请看我最近在网上读到的一篇文章,里面谈到“我的一个老同事曾经说Visual C++很臭,因为它不允许你在一个函数内拥有超过10,000行代码” 。

谢天谢地,幸亏他不是我同事,否则,我就算再聪明100倍,也还是逃不出被踢出局的命运。并由此第一次对VC产生了好感(不过我觉得它应该把函数长度限制设为更小的值,比如100行,这样才可能在一定程度上促进社区的代码改善)。

所以,我们只能以大多数程序员的平均接受能力为准。而根据相关调查统计,大多数人对于7 ± 2以内规模的事物有较好的控制力。


面向对象


事实上,当使用“面向对象”范式来进行软件设计时,如果你非常重视“消除重复”,“分离关注点”,“清晰表达”,那么很自然的,你会得到一大批短小精干的类和方法。即便你使用其它范式,比如面向过程或函数式编程,在相同的关注下,你也很难得到面条式代码

所以,曾经有人在演讲中提到:在面向对象系统中,你不可能得到面条式代码

你或许开始质疑:“我们的系统都是用C++或者Java写的,为何有那么多的 ‘面条代码’”?

事实上,“面向对象”是一种编程范式,与具体语言关系不大。使用面向过程的语言也可以构造出面向对象系统;反之,使用面向对象语言构造的系统,却未必是面向对象系统 。在面向对象思想的指导下,面条代码确实很难出现。而使用“类结构”却存在大量面条代码的系统,并非真正的面向对象系统。这样的系统,有一个专门的名字:肉团面(Spaghetti with meatballs)。


馄饨代码——better or worse?


但这并非故事的全部,那位演讲者的完整阐述是: 在面向对象系统中,你不可能得到面条代码,但却会得到馄饨代码(Ravioli Code)。

顾名思义,馄饨代码是指程序由许多小的,松散耦合的部分(方法,类,包等)组成

很明显,在那位演讲者看来,馄饨代码绝对不是一个褒义词,把面条代码重构为馄沌代码只是把一个问题变成了另外一个问题 。

而这样的看法并非个案。在咨询的工作中,我不止一次听到这样的反馈,馄饨代码相对于面条代码,更加难以理解和跟踪。

对于这样的意见,最初我感到非常困惑,因为这与我的认知和经验恰恰相反。但既然持有这种看法的人并非“一小撮不明真相的群众”,那就有必要站在对方的角度来思考一下造成这种结果的原因。

在这些工程师看来,在面条代码中,一个大函数尽管复杂,却完整的描述了整个过程或算法的所有细节。但在馄沌代码中,这些过程和细节被拆分的支零破碎,分散到不同的类和方法中,为了理解一个过程或算法,必须在类和方法间跳来跳去,如果有多态存在,你都不知道到底是哪个子类的相关方法被执行。

面对这种困惑的工程师们需要了解:面向过程和面向对象的思维方式和解决问题的方法有着很大的差异——


算法 vs. 机制


面向过程解决问题的思路是算法或流程,你会把它想像为一条流水线,或者把自己看作亲力亲为的CPU。而面向对象着重于机制的建立,你可以把目标系统想像成一台由许多零件构成的机器,或者一个良好运转的组织。

所以,对于一个面向过程的程序,你需要理解的是事物处理过程或算法步骤,每个过程都是无状态的,只有输入和输出(对于全局或静态变量的访问是面向过程的副作用)。

而对于一个面向对象系统,你需要首先理解一个设计的结构(类,类的职责,以及类之间的关系),很多在面向过程的代码中必须用过程来描述的“流程”,比如一些分支逻辑判断,在面向对象的系统中,靠类结构就已经解决了。在理解了结构之后,下一步需要了解的是类之间的交互。在明白了类结构之后,对于交互的理解就不再是件困难的事情。除非你的类结构本身就混乱,晦涩,难以理解。

你不妨想像一下,当你试图了解某个机构一个具体事务流程的时候,最高效的方法肯定是先了解它的组织架构,了解每个部门职责,以及部门之间的关系。在此基础上,再去理解一个具体事务的流程时,就会容易理解的多。反之,在你不了解组织架构的情况下,一上来就直奔一个具体事务,你可能更加希望一个部门,甚至一个人就把所有的事情都做了;拿着一份文件在各个部门之间穿梭盖章,肯定会让你非常困扰和厌烦。


分离 What & How


另外,对于馄饨函数的理解也需要不同的思维方式。以面条代码面目出现的函数,不仅仅在描述“做什么(What)”,同时还会呈现“怎么做(How)”。因此,一个函数内部必然充斥着大量的实现细节,从而导致阅读者只能依靠注释,或者通过对细节的归纳总结,才能最终理解“What”。而馄饨函数则是将两个关注点进行分离,在高层通过抽象来描述“What”,在底层通过展示细节来描述“How”,最终放在一起来完整描述一个算法。需要特别强调的是,为了能够达到描述What的目的,好名字非常重要。

这样的方式,应该更加科学,更加符合人类认知习惯。但同时也可以理解,对于某些已经了解了What,只想了解How的人,馄饨代码会额外增加函数间跳跃的成本。

但世上没有免费的晚餐,既然问题的本质复杂度就在那里,为之付出一定的代价就是必然的。 除非不再选择程序员作为职业,否则,我们只能通过评估各个方案总体上的成本收益比,来选择合适的方式。


馄沌代码—— 更好的成本受益比


另外,一个必须承认的事实是,“复杂性”是损害“可理解性”的面条代码的复杂性体现在一个函数内部细节数量和逻辑控制,而馄饨代码的复杂性则体现在函数或类的数量和结构

看起来我们只是将复杂性从一种形式转化为另外一种形式,但事实上,馄饨代码收获了更多,它的意义不仅仅体现在可理解性,还体现在可重用性灵活性,更加符合“高内聚,低耦合”原则。

所以,尽管 面条代码在现实中广泛存在,但对其却是压倒性多数的批评;而馄饨代码,尽管也并不完美,却在面向对象阵营得到广泛的支持,甚至被列为整洁代码的典范。


仅仅“小”是不够的


“短小的函数”并不意味着馄饨代码。在“高圈复杂度”被确认为是面条代码的特征之后,很多团队都定义了自己的“圈复杂度红线”;另外,一些团队也规定了单个函数“代码行数”的上限。但这样的约束,只能导致“短小的函数”,而“馄饨代码”并不仅仅“短小”,还要松散耦合,还要表达清晰

首先,即便一个函数只有一行代码,但也会由于包含了过多的细节而难以理解。不信,看看这个例子:

#include <stdio.h>#include <math.h> double l; main(_,o,O){ return putchar((_--+22&&_+44&&main(_,-43,_),_&&o)?(main(-43,++o,O),((l=(o+21)/sqrt(3-O*22-O*O),l*l<4&&(fabs(((time(0)-607728)%2551443)/405859.-4.7+acos(l/2))<1.57))["#"])):10); }

这个程序绝对可以通过编译链接,并且功能强大——能够用ASCII画出当前的月亮盈亏状况。技术上这个程序的函数主体只有一行代码,但其所包含的信息量之大,估计没有几个人仅仅靠阅读和分析就可以完全理解。
这个例子可能有些极端,那我们不妨看一个正常的例子:

return (0 < width && width <= 100 && 0 < height &&height <= 75) ?      height* width : 0;

这个例子并不非常晦涩,任意一个合格的程序员花点时间就能领会它的意图。但如果我们将其改成下面的样子,其容易理解的程度就得到了进一步的提高。

return isValid()? calcArea() : INVALID_AREA;

尽管我们通过提取函数和定义常量,增加了新的代码元素,但这种付出是值得的。

另外,如果一个函数的表述不具备“对称性”,或者不符合SLAP,那它就无法达到“抽象”与“细节”,“What”与“ How”分离的目标;就算这个函数非常短小,它也是晦涩的。

所以,我们真正的目标是“消除重复”,“清晰表达”,而不是“馄饨代码”,更不是“短小函数”。“馄饨代码”只是一个结果,而不是“动机”,而“短小函数”则只是“馄饨代码”的特征之一。永远“不要把解决问题的方法当作问题本身”。


消除不必要的复杂度


另外,由于“复杂性”会影响“可理解性”,所以,我们需要控制不必要的复杂度。那些不必要的抽象,不必要的函数,均不应该在一个设计中出现。所以,Kent Beck在“简单设计”原则中描述:

如果一个代码元素对于满足功能消除重复,或者提高表达力都没有用处,那么它就不应该存在。

在重复没有出现的情况下,对于“预先设计”所引入的复杂性,需要特别的小心和谨慎,究竟是这个“预先设计”更有价值,还是去除其引入的复杂性以提高“可理解性”更有价值?这需要设计者根据成本收益原则进行仔细的权衡。

但“重复”一旦出现,为了消除它而引入的复杂度,就是你必须要承受的代价。即便由此降低了“可理解性”,也物有所值。因为,一般而言“重复”比“难以理解”所带来的后果更加严重:“重复”往往意味着设计上的问题,以及维护上的高昂成本 。

所以,“可重用性”和“可理解性”并非“正交”的两个概念。但它们也并非相互排斥,水火不容。在“消除重复”的前提下,我们还是可以尽量提高代码的“可理解性”,更何况,事实上很多时候,“消除重复”的过程就是“提高可理解性”的过程。只有在少数情况下,当它们发生冲突的时候,我们才需要在二者之间做出取舍 。


原创粉丝点击