Are if-statements Really That Bad Though?
从设计模式与一段"Clean Code"说起

TL;DR

Context

某天在逛reddit r/programmingcirclejerk版时发现某知名作家推特上重构一小段if-else。(reddit link)该作家(与Jonathan Blow和Steve Klabnik等cult leader一起)作为r/pcj版的长期的嘲弄对象,这次他的发言也因为过于典型而被人挂出来嘲讽;然后人们发现他为此特意还写了篇blog,对条件语句大加挞伐,仿佛这种基础结构有非常严重的原罪。我想,要么就是此人注重他观念中的clean code注重到已经有点发癔症的地步,要么就是我会错意。无论是哪种情况,我认定我都应该仔细看看。

The "Clean Code" Way

Robert Martin的原话是:
Place the if/else cases in a factory object that creates a polymorphic object for each variant. Create the factory in ‘main’ and pass it into your app. That will ensure that the if/else chain occurs only once.
这段话太OOP了,需要换成正常的说法:

Is it really "clean" though?

这篇blogpost我是一点一点写的,到现在(2021.3.28)已经花了差不一个星期[4]。期间我也曾经怀疑过我有没有必要写下去:如果这种做法在某种情况下真的well-justified,那我其实不必持如此反对的意见写那么多话;但是经过仔细思考,还是认定我要把这些话写完。

The real thing

问题在于这根本就不是「让if/else消失」的方法;这只不过是将可能在很多地方出现的相同的if/else单独抽了出来而已,只是一个单纯的抽象操作。如果要再说,对象open与否其实也无所谓:由于这if/else的存在,如果需要进行扩展,你反正都是要改代码的,不过是改多改少罢了。

The potentially better

如果我们使用一般的数据结构(e.g. tuple/map)而不是“OOP”的class hierarchy或者interface/implementation,这种"open defunctionalization"本质上可以视作某种table-driven method;甚至,如果我们用的语言支持函数作为一等公民,我们可以直接使用函数而不做defunctionalization,e.g. 我们可以这样:

var action = {
    apple: () => {
        console.log('performAppleAction');
    },
    orange: () => {
        console.log('performOrangeAction');
    },
    cherry: () => {
        console.log('performCherryAction');
    },
}[someTypeInputResult];

// then we register more action by `action[typeName] = actionF` and
// perform action by `action[typeName]()`.

虽然说这跟switch case也没有多大的差别就是了,多出来的好处也就只有能够随意加handler这一个;需不需要这个好处依然是需要先看这个片段的context才能判断。

Are if-statements really that bad though?

再到我们的Uncle Bob对分支语句的那篇檄文。我一开始还在想,如果这只是个单纯的Mapping也要搞这些鬼东西吗?真就觉得内存随便用[3]心智负担随便堆?还好他对此的回答还是正常的,不至于像他的门徒(或者任意的design pattern fanboy)一样发疯:

Firstly, if the sole intent of the programmer is to translate:
0->'male', 
1->'female' 
otherwise -> 'unknown'
…then his refactoring #2 would be my preference.

虽然说这种情况更应该用map和带default value的lookup;它是一个mapping,它就应该用mapping的做法来做。

然而他宣称分支语句具有更加深刻的问题:

Such statements tend to have cases that point outwards towards lower level modules. This often means that the module containing the if/else/switch will have source code dependencies upon those lower level modules. That’s bad enough. We don’t like dependencies that run from high level modules to low level modules.

而且问题不仅如此:

Other higher level modules tend to depend on the modules that contains those if/else/switch statements. Those higher level modules, therefore, have transitive dependencies upon the lower level modules. This turns the if/else/switch statements into dependency magnets that reach across large swathes of the system source code, binding the system into a tight monolithic architecture without a flexible component structure.

简而言之,(1)由于时常会有人写出依赖于低层模块的分支语句,因此是分支语句是坏的;(2)由于时常会有人写出依赖于这些带有分支语句的模块的高层模块,这种坏处也因此传递到高层,整个系统就十分被动,不够灵活。说实话,我个人认为,这并不是分支语句的问题:如果你的设计导致你真的需要用这么一种方式去维护所谓的dependency,那么恐怕最好还是首先重新思考一下相关片段(或者甚至整个程序)的设计本身;而如果这样的设计在context下是完全合理的,那么就不该做这种不必要的重构。无论是哪种情况,使用他的方法要么只会把更加根本的设计问题用clean code给模糊掉,要么就是在把原本清晰的设计用clean code给模糊掉,哪一种情况的结局都不clean。

No Pattern, No Nothing

No Pattern

Robert Martin的这种"clean code"也好,还是别的使用设计模式的方式也好,都会带来两个非常严重的观念上的问题:

我认为:

也许是因为我没做过大型的活,也许是因为我做过的活还不够enterprise,我没有学过所谓的23种设计模式,也没感觉到有特意学习和记忆的必要;我只是一直觉得,所谓的设计模式是会在设计和编码的过程中自然产生的,你不需要走向设计模式,设计模式自己会走向你;甚至有的时候,设计模式完全可以不用存在,因此也没有出现的可能。比如说,我第一次以"pattern"的名义接触到pattern是在大学时一门叫「数据库工程」的课,课的主要内容是用C#和ASP.NET连接MSSQL写非常简单的webapp和客户端app。我只是庆幸它只是选修,至于学校为什么会开这么一门课教这么一些内容,我可是至今都搞不懂。总而言之,到了后来,老师开始讲工厂模式和抽象工厂模式,说为了能够用同样的方式构建出不同的对象,需要为此单独设计“工厂”或者“抽象工厂”类。可是……为什么?这是C#,我完全可以直接delegate啊?但是考试不仅要考,而且还要背,还要手写代码,只好硬着头皮去熬。直到很久之后我才意识到,它其实并没有那么不合理:如果实在没有能用于实现高阶函数的机制,如果你不能把类或者构造函数或者某个帮忙构造的函数当一等公民传来传去,想要做generic的构造方法,当然会需要这么一个东西;但这也同时说明它的诞生完全只是因为语言的能力不够强,要不然把这些东西本身传过去就是了,为什么要绕这种远路呢?

当然,有一些「模式」我是通过别的方式得知的,比如说observer pattern可以视作某种reactive programming在低层视角的样子,interpreter pattern可以视作某种颇为刻意的defunctionalization等[2];但是我一直以为至少对我而言,这些模式要么能从我用过的语言里的一些概念简单推论而来,要么就是我一直都这么写而从来想不到需要单独给一个名字,要么我从不这么设计我的程序;而无论是哪种情况,都不需要记忆固定的分离/组合套路(更加不需要UML)。后来我看到有人在某处讨论里贴了这个地址,提到许多Java的"pattern"在clojure里可以是再也平常不过的函数和语言特性;Alan Kay也作过类似的演示。有人看了SICP和LISP后会觉得自己受了天启这种事情,其实也并不难理解。

我又看了一圈,发现设计模式中「模式」一词来自于一本叫A Pattern Language的讲建筑的书。建筑?先前我并不知道设计模式有这样的背景,我应该稍微去看一下。

我并没有把A Pattern Language读完(那本书足足有1200+页),但是得知书的作者Christopher Alexander曾被邀请到OOPSLA作演讲。也许人们以为即使他是个与软件工程并不很相关的建筑师,作为导致了设计模式的诞生的著作的作者,他对软件工程一定会有不凡的见解。我读完了演讲的全文,讲的其实也还是更加偏向于建筑的内容;他在会上讲过他对Pattern language的初衷:

First, it has a moral component. Second, it has the aim of creating coherence, morphological coherence in the things which are made with it. And third, it is generative: it allows people to create coherence, morally sound objects, and encourages and enables this process because of its emphasis on the coherence of the created whole.

接着他又坦诚说,他并不知道软件工业是如何朝着这几个目标前进的:

I don't know whether these features of pattern language have yet been translated into your discipline. Take the moral component, for example. In the architectural pattern language there is, at root, behind the whole thing, a constant preoccupation with the question, Under what circumstances is the environment good? (...) I do not know whether that sort of moral component exists in computer science, or in software engineering, or in the way in which you do things. (...) That is what we were after. I don't know whether you, ladies and gentlemen, the members of the software community, are also after that. I have no idea. I haven't heard a whole lot about that. So, I have no idea whether the search for something that helps human life is a formal part of what you are searching for. Or are you primarily searching for—what should I call it—good technical performance? This seems to me a very, very vital issue.

"Helps human life"? 他恐怕要非常失望了……

Alexander在演讲中提到Pattern Language其实并没有达到他想要的目的,于是他写了The Nature of Order。在网上看了一圈The Nature of Order中Alexander对构建的基础要素的描述(link1 link2)后,我疑心那理论只对所谓"structured beauty"「结构化的美感」的东西适用。譬如说音乐:建筑有strong center,音乐则几乎总是有核心的motif;建筑有alternating repetition,音乐有变奏,指的就是alternating repetition;建筑有contrast,音乐则有对位法的contrast和变调前与变调后的contrast等等;但是这些事物在软件工程的对应物则不甚明显:建筑里的void可以是一片故意设计出来的没有实际用途的空间,音乐里的void可以是作曲家故意留下的没有声音的空隙,可是软件里的void究竟是什么?……需要实现的部分总不能留出空隙吧?软件里的contrast又应该是什么跟什么的contrast?总而言之,我疑心Alexander的本意是想找出能够稳定产出structured beauty的方法,这方法(无论存在与否)是跟美感有紧密联系的;而软件工程有自己的一套规则,无论是这套规则还是软件工程本身,都跟他试图捕捉的美感无关。

我反倒十分好奇,因这本书而被全世界的程序员如此推崇的四人帮,他们知道他们的模式只不过是这种对别的领域对结构化的美的追求的笨拙而别扭的模仿吗?

No Nothing

在看过这么多hype和对hype的解构之后,我想,这也许是一个语言问题:这些事情本质上都是描述性的,是一种描述(不管这描述有多么模糊),而描述本身绝不应被上升成某种非常神圣的东西,譬如说所谓的OOP,所谓的函数式编程,这些名词也许正好属于「是描述但不够格成为predicate」的一类,因此把这些名词作为predicate使用的讨论没有意义;而过分执着于这种单纯是由人工的想象构建出来的predicate性,最后的结局就是「流派未月亭」:

(……)流派未月亭。这就是编程的本质,编程的流派,编程的范式。流派未月亭。就是如此荒谬,毫无意义。只要我们一天还在用着‘范式’去讨论编程语言,我们就会永远陷入这种情况:(……)永远。大家只是互相竖起远离实际的符号,互相实施稻草人謬誤。(……)又或者,我们也可以谈论各种effect system(……)而不是,谈论到mutability,就只会'immutability是大势所趋,implicit parallelism大法好',又或者'immutable不符合计算机基本模型'等myth。这样有啥坏处呢?为啥我们不这样做?我不知道。也许,这样我们就不能喊些看上去很酷的名字,就跟圣斗士不能喊天马流星拳一样

一定要在软件工业里保持冷静,一定要学会敌视对名词的hype。比如说,什么single source of truth?不过是global variable in disguise。前20年天天说全局变量坏,现在又来搞了?Bullshit。它functional又immutable(e.g. Redux)所以它不一样?It does not matter, 谁不知道functional和imperative一体两面?很多东西都是fluid的,都是flexible的,用可以用,但是hype真的不仅没有必要,而且有时甚至有害;也许下一个在"Hacker" "News"首页出现的前端框架只不过是作者为了KPI或者公司内的话语权之类的鬼东西特意糊弄出来的、达成了它的“政治任务”就会被抛弃的稻草人,或者也有可能是刚刚离职预备跳槽的某人为了填充简历而搞出来的花架子;我可不愿将宝贵的时间浪费在这种东西上。

一点不相关的后记

  1. 我讨厌原本问问题的那个人;通过他在那个推特thread的言论,我断定我与他处不来,也决不想与这种人共事。他先后问了几个问题,有些完全可以变成简单的mapping,有些不是特别适合直接变成mapping。但这些不是重点,重点是他的这种问法很有问题:一个做法clean不clean需要不需要重构跟它所在的context密切相关,你不能因为没有通用的强有力的做法而推一个因为在各种场合都没什么用所以在各种场合都能用的做法为silver bullet。我讨厌这种围绕这种人造的"silver bullet"进行催眠的cult,更加讨厌身处cult和向外宣传cult的每一个人。
  2. Robert Martin在推特上的头像是一个「已认证clean code」标志。坦白说,如果一个人的自尊大到能够让他用这种标志作头像,我还真的不想跟这个人有过多接触;这当然是我个人的偏见,比如我有时会特意躲避玩英雄联盟的人,躲避用多拉A梦头像的人,躲避用太极头像满嘴易经的人,躲避年过35的中年程序员——尤其是年过35的中年程序员,为的只是尽量不要在(至少是潜在的)没法讨论道理的人身上浪费时间——我已看过足够多的一见我年龄还没有2字头就大呼我幼稚不要质疑他重复了十余次的一年经验的中年程序员了;我与我年轻时就接触PLT的同僚,在与人讨论语言时就常常受这种指责的。并不是我在这方面的知识有多渊博(我真的没有多渊博),是对方时常是连基础的认知都极度匮乏的人。
  3. 软件工程是一门只在表面跟计算机相关的完全是关于人的学问;我喜欢计算机,但我不喜欢跟人打交道。也许就是因为这个,人们可能会觉得我(以及所有因为喜欢才学计算机的程序员)很幸运能够靠自己喜欢做的事情吃饭,而我却一直对这些东西抱有一丝厌恶。
  4. Martin写的那个筛法求素数看得我想揍人。也许不是他本人写的,但不妨碍我将认定这份代码clean的人视作我永恒的敌人。
  5. 当然,将所有与自己的认知相违背的理念直接打上「幼稚」「缺少经验」或者「无知」的标签加以忽视,确实要比认真思考方便舒适得多。但是我问你,这样便捷廉价的舒适,于你又有什么意义?

Footnote

  1. 我这里的defunctionalization指的并不完全是“传统”的(as in Reynolds 1972)意思,而是指在函数本身不作为一等公民的语言里让函数间接成为一等公民的手段(e.g. function object)。也许这就是defunctionalization;我没有读过原始论文,不知这个词在相关context里是怎么用的。
  2. 或者某种同样刻意的DSL。defunctionalization也好interpreter也好,在人们觉得他们需要用interpreter的场合,这些技巧背后都有一个统一的想法:只要有interpreter,数据就可以当函数用;如果数据能做到函数做不到的事情(e.g. 一等公民、可组合等),那么这么做就有赚到。
  3. 或者可以再用一下所谓的singleton pattern,但这又是新的心智负担了。
  4. 我中途又搁置去干其他事情,于是至今已经近三个月了。

2021.6.8
Back