从模式匹配与类基多态之间的选择说说项目的良性成长

一个选择问题

网上可以找到不少讨论这个话题的文章(如果你不知道为啥这两个东西要放一起比较的话,建议你先读懂以下这两篇,或者跳到下面讨论重要及解决方案的部分):

本文不讨论模式匹配和多态是什么,这种知识性问题。本文只想先讨论一下,如何正确合理地使用这两样东西。

因为了解一个东西,其难点从来都不是知道它是什么,它有什么优缺点。这些都是非常简单的。真正的难点是,拿到现实场景中,分析出并现实中需要考量相关因素是什么,分清主次,并做出合理的选择,以及,最容易被忽视的:接受相应选择背后的成本。否则就会出现,知识性的东西说起来头头是道,但是就是用不对地方的问题。举几个反面教材来做热身。

场景一:

A: “我这个类有好多字段,写个equals和hashcode方法都要写半天,apache commons用起来也不方便。”

B: “用data class啊,自带了equals和hashcode的实现啊。少撸好多代码,多Clean Code啊。”

场景二:

A: “这边儿处理各种税费的case好多啊,处理起来好乱啊。”

B: “用case class啊,然后用模式匹配给各个情况分别处理,多清楚。”

你看,这一来一回的字面上的逻辑都是通的。但是其实逻辑狗屁不通。 或者至少是没用到点子上。

做正确的选择并承担成本

说了一堆废话,还是开门见山地陈述一下自己的观点。

多数文章都会讲到:“模式匹配,便于扩展功能;多态,便于扩展类型。”但是从来不会提到前提及代价是什么。所以我想补充一下。

关于模式匹配:

  • 用对的前提是,维护好功能实现的完整性。一个功能就用一个函数或函数组实现。什么意思?意思就是当一个新的类型加入的时候,直接修改现有代码分支,而不是在外面包一层,谓之不动老代码,安全,还冠名Proxy啦,装饰者啦来justify这个拙劣的决定。这也展示了一种常见的出品屎一样的代码的方式:一个代码库中,不同的开发者,把不同设计理念杂糅在一起,相互交织。每个人的想法单独拿出来或许都是对的,然而不那么上心地堆放一起,就变了一坨屎。
  • 另一个前提是,最好用在类型真的不大会新增的场景下。否则不是给自己找麻烦吗?这个说起来容易,到具体问题上就容易抓瞎,一年有四季,不会有五季,这种一看就适合用模式匹配。屋子里的房间类型,客厅厨房什么的,也还行。但是如果产品拍着胸口说句话,税费就这几个类型,然后你就开心地决定用模式匹配了?
  • 代价是,以整个函数为单位实现功能,功能就是封闭在函数内的,你没有办法在不修改现有代码的前提下,修改一个现有的功能。这算什么代价?代价要看场景,我举个例子,插件体系。基本都是通过新加类来扩展功能的。没有哪个插件是通过动态修改主体函数内实现来扩展的,因为不好改啊,有也是Hack,需要很底层的机制,成本也很高。所以没有哪个插件体系是基于模式匹配的。当然还有别的场景,引入模式匹配的代价也是完全不可承受的。要具体分析。

关于类基多态:

  • 前提是,不要对“便于类型扩展”有什么误会,如果是那种会产生类爆炸的场景,无论是模式匹配还是类基多态都是不合适的。你可能需要的是数据化,也可能更需要重新思考你的类体系设计。
  • “便于类型扩展”的代价是,同一个功能,其实现逻辑,会散落在各个类中。于是会相对比较难以从代码上,获得对某个功能的完整的全局的认识。但是这不应该是你声称一个代码不可读的借口。多态的代码读不懂,是你自身水平够的问题,不是写代码的人写得烂的问题。

我们选择一个东西好的一面的同时,事实上也同时必须承担相应的成本。选择不仅仅要看正面的效用,也要乐于接受其负面的损耗。最怕是什么呢?两个方式的好处想全要,又想易于对类型扩展,又想易于对功能扩展。讲道理鱼与熊掌不可兼得,他开启杠精模式,说那是你水平问题。也是,我的确没有水平创造出这样一门全新的语言来。

这种选择有多重要呢?

最后一个问题,这个选择,把这俩东西用对这么个事儿,有多重要呢?

从现状来看,一点不重要。我这工作十来年了,从来没见过哪个公司的Code Review(如果有的话)中,大家会聊到如此深入的设计理念上的细节。为啥?因为即便你用错了,如果就那么几个地方,加加班改改就是了。如果用错的地方太多,而且错误又不限于模式匹配用得不对这一个问题,就干脆立项重写好了。把产出垃圾代码的锅甩给前任,现任人员从上到下都是功绩。所以没人死扣代码质量。况且,也不是所有人都喜欢讨论这种问题。有的人喜欢死扣细节,有的人还不高兴:“没bug能work的代码,非他妈要讨论写成啥样好,还讲一堆大道理。早点回家吃饭不好吗?蛋疼。”。

然后呢,重写后的第一版,一般都是好的。但是之后,就也走向越写越烂的老路了。然后又是新一轮的重写。最终结果呢?每一轮产出的都是垃圾。如果公司业务良好,走在快车道上,不差钱,倒也不在意这些重写的损耗,只要每次打完补丁,每次重写完,都能满足业务要求就好。人不够就加上嘛。有人什么垃圾代码搞不定?但是如果公司处于一片红海,整体经济环境又不乐观呢?一个人写垃圾危害也许还可控,如果一个公司整体风格就这样,那这些拙劣的设计和代码,迟早会拖累公司的成长速度。

还有一个问题是,如果一个人,其工作涉及到了模式匹配,多态的使用,但是他都没有办法正确使用的话,你如何相信这个人,能正确地使用各种设计理念更加复杂的框架与中间件呢?比如Spring Boot, Spring Cloud, Kafka, RabbitMQ。我是不相信的。

还有一个问题是,如果一个公司,对这些完全不在意,不去鼓励甚至要求员工去关注细节,一些员工就会觉得没有成长(尽管满眼的机会他看不到),没有技术含量(尽管一堆的技术问题没有解决)。然后这些一般般的员工也就离职了。优秀的员工是什么态度呢?他们这种嗅觉敏锐的,会第一批走,才不会甘心在一滩烂泥里同流合污呢。剩下的,就是没有丝毫追求,没有技术水平的员工。毕竟他们找新工作一般也比前两种人困难。 当然这是里糟糕的结果,也比较少见。我干了这么多年也只是听说过些故事。大家当段子乐乐就好。

解决方案

写了这十多年代码,对于如何解决这些问题,小有一些心得。如果一个团队的人,可以做到如下几点,是可以遏制项目腐烂的趋势,如果都做得不错,项目才能长期地向上成长。(如果这个公司的战略需要这个项目长期存在的话)

  • 不要Patch它,Fix它。我是这么判断是不是打Patch的。当你去改代码的时候,脑子里萦绕着“尽量不要动老代码,仅仅针对发生问题的这个case就好”的时候,你就是在打Patch。在以快速和不影响现有业务的幌子,行无脑不负责任编码之实。因为真正Fix一个东西,是要从这个问题是如何产生的开始,到仔细考量最合理的解决位置,以符合现有代码的设计思路的方式来处理。这个工作量与Patch不可同日而语。但是一个维护良好的代码库,Fix一个问题的成本永远是可控的。只有失控的垃圾代码,才会陷入“要从根本上解决这个问题,差不多要重写。”的窘境。
  • 认真地读现有代码,读懂。这是找到Fix方案的前提。如果你代码都没看懂,那碰到NPE的时候,那的确是,除了加句if not null,打个自己都不知道行不行的Patch,你的确也做不了别的更多的了。如果你感觉现在这个代码太垃圾,那太好,你展示才能的时候到了不是吗? 一般烂代码都是好读的,因为写的时候就没用脑子,只是分支多些罢了。设计精巧的代码才不好读,因为写的时候就是花了脑子的,你也不用指望自己脑子都不过就能轻而易举地看懂。如果你连烂代码都读不懂,那我建议你转行吧。读代码是基本功。
  • 编写有效的测试。如果你连有效的,能发现代码变更产生的非预期的bug的测试都没有。你当然也是不敢动现有代码的。如果没有测试怎么办?太好了,你可以通过写有效的测试来尽快熟悉这个系统的功能。另外,光看代码覆盖率是没有用的,我可以轻而易举地写出100%覆盖,但是实质上毫无用处的测试代码。
  • 持续改进。这个涵盖比较广,细说能独立开一篇文章。但是这部分才是解决根本问题的精髓。不断地发现问题,解决问题,一个系统才能成长。把问题隐藏掉或是绕过,都不是长期的处理策略。这里的重点在于追求卓越的心态。比如,抱着“大家都是这么搞的,所以我这么搞就可以了”的心态,是没办法改进的。要持续改进还包含要实事求是。事实是什么样就是什么样,不看人,不双标。不要因为这个代码是CTO亲手写的就不敢动,不要因为一个需求是CEO提的就不敢讨论。自己到底在什么位置上都看不清,对与错都不敢坚持,是没办法改进的。

但是问题是,我还没有见到过哪个团队的所有人,都能做到如上所有点的。毕竟,每个人写代码的动机,都是不一样的呀。

发表评论