分支语句、面向对象,和心智包袱
今天想查查if...else...和switch相互嵌套怎么写,不是一般的嵌套,就是if...的大括号嵌入case分支(其中if本身在switch之外)、交错起来那种。关键字把我引上了好久没来过的博客园,进入了某个技术很不错的老伙计的博客。
呃,结果我不但没找到想要的东西,又看到了if...else...或switch如何如何不好,应该进行面向对象封装和重构的文章。简而言之,就是switch的case块中含有了很多分枝的if...else..。作者毫无例外地给重构为了工厂模式,先case某种情况,然后用工厂返回一个command执行(多说一句该作者的例子是javascript,要是过去的我会用高阶函数而不是面向对象方法)。我想说的是,一般情况下把case里面直接换成一个最普通的函数调用不行嘛....
当然,如果真的会面对可控的变化,很显然封装可以帮助我们再今后面对它们时更轻松的处理。但拿这样一个例子来*一般性的*吐槽分支语句们,等于说因为坦克能开炮而汽车不能,所以我们都应该去买坦克。
代码 vs 数据
无论封装出来的多态也好还是“硬编码”的分支也好,这些形态其核心在于存在前提和后续之间一一对应的关系;但是不是所有一一对应的关系,都应该使用某种高大上的方式实现呢?不少人都知道把程序逻辑“数据化”变为可配置的东西,但什么叫做“硬编码”?
代码即数据。这不是很多高级人士普遍在讲的代码和数据是一回事、是一个问题的两种表达,而是说代码本身就是数据。用户操作的数据被我们的程序吃掉;吃进中高级语言这个数据的是编译器;吃进二进制机器码这个数据的是CPU及其配套的执行机制。无论是CPU、编译器还是我们自己的程序,当它处理的数据一旦黑纸白字的写在那里,就成为了硬编码,并不因为这数据是XML或者机器码而有任何不同;区别只是我们会在项目生命周期的哪个阶段对这些“硬编码”进行改动?显然在外围改动的能力需要内部提供更多的支持,这是需要成本的。
可配置
所以考虑这个问题的时候,关键在于需求:我们需要在什么场景下变化?在将会部署的目标环境中,要求运行时改变程序行为,我们肯定要多态+装载器+用户界面;不允许重新编译、或者重新编译的代价过高,我们就使用多态+配置文件;有些部分的程序代码可以重新编译、或者作为动态语言且客观允许在目标环境中修改,那么我们也许只是独立出一个配置模块,因为这种方式成本最低且最灵活;产品可以直接在目标环境中不断迭代且没有什么因素让修改代码成本过高,那我们也许根本不用费那个心思,很多互联网公司发布新版本就是这种情况。
归根结底,进行配置和写程序代码,都是提供一种对任务的描述;这没有什么本质区别,除了后者有编译器给你把关避免一些低级错误。澄清了这个事实就可以把精力集中在剩下的方面:在没有什么特别需求或限制的时候,程序设计和实现的关键在于可读性。
“可读性”
作者的博客里明确地说道,重构的目的是“将复杂逻辑运算打散并分布到不同的类里面”;最让我担心的,是文章后面的留言大家里也倾向于该作者的思路是正确的。关于可读性,上述文章的作者也提到了:他还说else是排除了所有其它分支的条件大杂烩所以“难以阅读和管理”。
这种说法更像是欲加之罪何患无辞:当我们用面向对象手法使用默认策略对象时,这个默认策略不也排除了其它针对性策略的前提条件了吗?那么凭什么封装了之后就好懂,不封装就不好懂呢?从根本上讲,如果一堆if...else...的代码中若不包含无效的废话,那么我们是不可能消除它们背后的内涵的;重构仅仅是把分支换个地方或者换个形式;在程序在满足需求和限制的基础上,问题是什么样子阅读门槛最低?
如今每个程序员都被告知面向对象的可读性高,但这种声明是极其可疑的。我过去参与内核移植的工作时,读了不少linux代码;弄Android中间层时,又读了不少中间件的代码。可以说Linux代码大部分都非常易懂,而那些处处封装的中间件代码,有些不挂着调试器走一遍,就如同进入迷宫一般,根本不知道哪是哪。你以为该吃饭了,结果对着一个茅坑,这不是什么新鲜事。
完整、明确、有层次
说实话,上述文章的作者作为一个工作在某个过去并不太重视方法论的领域,在程序设计方面缺乏经验可以理解。问题是在这种情况下,他甚至在原文中说出“分支超过两个”就不应该以“硬编码”的形式出现,也太武断了。
如果只是嫌代码太乱,那么把分支之后都改为一个函数调用,自然而然能避免嵌套、缩短行数,让代码从“乱花渐欲迷人眼”,变成“浅草才能没马蹄”。再进一步把if后面括号中的判断,放进有明显意义的函数里,就变成了和excel表格一样易懂的东西。在表达一组对应关系的描述中,把所有细节在当前层次隐藏,每个条目无论在字面意思上还是排列形态上也同时具有了明确性。
我们处理的很多逻辑,只有写在一起才能一目了然;如今很多语言提供异步操作关键字以避免完成时回调的两段式写法,就很好地说明了对完整性的需求是广泛存在的,且已经重要到在一些情况下最好由语言直接提供支持的地步。具体到分支语句则可以非常好的把一一对应关系展现在眼前:只要A->B的形式没变,任何其它同样完整的表格都不会有什么哪怕表面上的差异,最多是换几个不同形式所要求的不同字符罢了。
在实践中若不存在运行时改变行为的需求或无法重新编译的限制,保证一一对应的关系每个占据一行、能够自我说明且不包含冗余信息,分支即便再多(比如一万个)也不会比配置性代码或者XML文件难懂;而对比拆的七零八落的精巧设计,具有完整性的直接描述显然更具有优势。
其它选择
事实上我倾向于,对于比明显的对应关系更复杂得多的情况,我们也无需使用那些臆想中的“让程序更易读”的方式把一件事拆成很多片段以管理每一次的阅读量。只要我们没有运行时改变行为的需求或者无法重新编译的限制,我们就没有必要使用多态等面向对象方法并承受它们额外的trade-off,甚至连拆出函数都不用。
像复杂数据的解析,将这部分工作抽取出来之后(可用面向对象或C++模板式的泛化甚至宏实现,还是看需求和限制),具体的代码完全可以生成。比如可以用javascript写一个页面,直观的把要处理的单位表示为方块,通过图形界面安排次序,通过form输入相关信息,然后生成C/C++或任何语言代码;为了反向生成图,还可以使用中间文件记录布局信息,或直接从生成的代码获取信息,仍旧用javascript展示出来。这些并不难,其成果(在这个例子是图)比代码直观得多,而且这样一个工具它的可复用性也要比各种代码级别的抽象来的广泛得多。
可读性
这里面关键在于,臆想中“更易读”的方式,只是对正在设计和实现程序的我们自己而言。当我们一点一点的分析问题,并绞尽脑汁完成一个“完美”设计的时候,我们彻底的理解了眼前的一切,并让东西和我们的思路保持一致,这时候我们自然觉得自己的作品很易读。但是因为其它人没有走过这个思维路径,甚至可能一时走不出十分相似的思维路径,我们怎么能期望他们快速理解、并且可以严格按照作者设定的方式去继续维护呢?
实践中,这些Linus所谓的“漂亮的坚固堡垒”也一点不好读,哪怕对于面向对象重度中毒者也一样;这些发烧友只会一遍又一遍的揣摩作者的心思,等终于明白了感叹一声“真TM精妙”,而完全不在乎他自己被作者折磨的好似滚了五十遍床单。类似这种自从面向对象培训业火了以后越来越多出现的情况,只能是Linus所谓的心智包袱之一。过的时间越久,我就越体会Linus说的,“哪怕只为了把这些人排斥在Linux核心小组之外这样一个理由,就决不能使用C++”。
相反,if...else...也好switch也好,只要我们有心把它们安排得漂漂亮亮,它们就会像只有两个字段(条件,动作)的数据库那样简洁;由于对应关系得到明确的展现,它对任何人哪怕程序外行都是可读的。除此以外,我们还有好多直观展现信息的方式,比如上面讲的数据解析“网页”,听起来似乎多了一道手续,但写这样一个东西绝对不会比直接上方法论神油折腾复杂逻辑更加费时,却获得了完全贴合问题的模型及操作工具,可读性和可维护性成倍提高。
最后
当然,我们需要封装时,不可避免的必须封装。此乃一句废话,也是真理。那么何时需要封装、多态或者任何其它方式方法?最后重复一遍:当需求或者限制明确的指出这一点、而不是我们自身的心智包袱暗示我们这么做的时候。作为从面向对象走过的人,我深刻的知道这种暗示有时会多么强烈,我们真正要做的就是暂时拒绝诱惑、缓一缓、再缓一缓。
对了,上面有“可控的”三字加粗换颜色了,那也是一个重点;对于不可控的变化,任何设计都是盲人骑瞎马,所有的精力投入连个响都听不到。好久不来,向所有的老伙计问声好,不知有几个还在?貌似不少人事业都蒸蒸日上,恭喜了 :)