摘要:探究事物的反面性是一种哲学上的思考,物理学在“物质”之上提出并证实了“反物质”的存在,就像数学上有正数也有负数,武侠小说中有九阳神功也有九阴真经,生活中有婚姻也有出轨,事物总是充满这种相互矛盾而统一的有趣现象。那么放在GoF提出的23种提倡使用的设计模式上,是否也存在反设计模式,或者反模式的存在呢?答案是显而易见的。而且从辩证的角度来看,一个设计模式在特定的场合下是积极并且优势显著的,但是在偏离最佳适合场景时,它本身就转变为一个反模式,从而导致不良影响,就像现实世界中没有所谓纯粹的好人或者坏人一样。托翁说:“幸福的家庭都是相同的,不幸的家庭各有各的不幸”,放在这里,则是“设计模式都是相同的,反模式则各有各的问题”。
1.反模式的历史
根据维基百科的定义,设计模式是一套被反复使用,多数人知晓的,经过分类编目的代码设计经验的总结,是软件设计的某些特定场合的某些问题的解决思路。是前人经过大量的实践,总结出来的无论从效率、扩展性、复用性、可靠性等方面都显现出优势的解决思路。反模式(anti-pattern)指的则是在实践中明显出现,但低效或有待优化的设计模式,是用来解决问题的带有共同性的不良方法。反模式经过研究分类,能在研发系统尚未投产时被辨认出来,防止日后重蹈覆辙。
Andrew Koenig在1995年首创了anti-pattern这个词,灵感恰恰就来自于GoF的著作《设计模式》,是这本书首先在软件领域引入“设计模式”的概念。三年后anti-pattern因《antipatterns》这本书获得普及,它的使用也从软件设计领域逐渐扩展到了日常的社会互动中。按《AntiPatterns》作者的说法,区分反面模式和不良习惯、错误的实践或糟糕的想法可依据至少两个关键因素:
其一,行动、过程和结构中一些重复出现的,乍一看有益但最终得不偿失的模式;其二,在实践中证明且可重复的有清晰记录的重构方案。
很多反模式只相当于是错误、咆哮、不可解的问题,或是有可能避免的糟糕实践,它们的名字通常是一些用反话构成的词语。有些时候陷阱(pitfalls)或黑色模式(dark patterns)这些不正式的说法会被用来指代各类反复出现的糟糕的解决方法。
反模式其实体现的是一种积极反思的行为。通过对不断出现、糟糕透顶的解决方案反思之后的深刻总结,让我们能够从错误或者失败中学习提高,避免出现相同或类似的问题,提升效率。理解了反模式,有助于我们在实际工作中预防并改正它们。
有一篇文章,讲到用5种设计模式来完成一个Hello Word的打印,使用关键字“write Hello Word with design patterns”谷歌一下就能找到(请使用正确的姿势访问谷歌),甚是有趣。然而有趣背后的反思,却是“为了设计模式而设计模式”,成了一种典型的反模式“为了XX而XX”。
从图书网站上,也可以搜索到一些反模式的书籍,比如《测试反模式》、《SQL反模式》、《Python反模式》等,连最近很火的微服务也有了反模式,这些书中都总结了一些最差实践,供我们学习并避免。
2.反模式中的神仙大类(倚天剑)和黄金大锤(屠龙刀)
反模式有很多种,我这里想给大家介绍其中的两种,分别是神仙大类和黄金大锤,我这里把他们比喻成倚天剑和屠龙刀,在于这种大杀器一般要谨慎使用。因其威力太大,用得不好的话,就极可能伤及自身。或者没有伤到自身,伤到边上无辜的花花草草也是不好的。
神仙大类,又称God Class, Blob Class等,是一类拥有太多属性和方法(比如超过20个)的模式,它能处理的事务涉及方方面面(比如员工类,涉及工资计算、税务计算、入职离职、数据库读写、请假报销等等),它所占用的代码行数从数百到上万行。“神仙大类”是KISS原则和SRP原则的反模式。我自己虽然未曾有幸见识过上万行的大类(倒是听说过不少),但确实见过超过2000行的只有一个main函数的大类。针对这种大类,我是该献上我的膝盖还是我的口水呢?我想刚开始是膝盖,因为不明觉历,后来铁定就是口水了,因为我不幸接手并负责维护它,每次只要一改它,在上线的时候我都会心惊胆战,晚上睡不好觉。
当然,对于一锤子买卖,如不再需要维护的代码,或者需求绝对不会变更的代码,这种神仙大类就让它逍遥去吧。唯一的不足之处就是,不能为下一个项目提供可复用的单元,只能看着年龄在增长,技能和效率却没有什么提升。普通程序员只是年复一年地完成日常的业务需求,没什么代码复用可言。好的程序员却可以在完成日常业务开发的同时,不断地总结并丰富自己的代码工具箱,代码复用率很高,真正需要写的业务代码也写得非常少,有时只需要做一下配置,就可以完成类似的事情。
神仙大类的拥趸者说,你看,我一个大功能一个大类就搞定了,一个大类就一个文件,如果按照你的那套所谓的SRP/KISS,至少要不下20个类的小文件,我不也算符合简单化原则了,一个文件还不算简单嘛。乍一听,我竟无言以对,如果你一个类文件里面也是分了各种层次,做了各种不同抽象设计的话,好像不无道理。只是,这种情况下,一个文件里面那么多功能,如果想重用其中的一个,咋办呢?是不是得把整个大类照单全收,还是把要重用的那个小函数拷贝一份出来?只是同一段代码一旦重复拷贝,就违反了DRY(Don’t Repeat Yourself )干燥不渗水原则,而被 WET(Write Everything Twice)湿漉漉反模式给狠狠地砸脸了。
神仙大类,本质上就是一个“集大成”的大胖子,在这个以瘦为美以减肥为时尚的今天,确实不受欢迎。你虽然不能像林丹那样拥有8块完美腹肌,但是你可以让你的代码做到啊,只需要远离神仙大类,或者使用“人挡杀人佛挡杀佛”的重构“拆”字诀把遇到的神仙大类就地拆成大约8个各司其职的小类就可以了。
黄金大锤,Golden Hammer,指使用相同的工具、产品或技术,解决几乎所有的问题。如果你只有一个关系型数据库,那么任何问题都看上去是其中的一张关系表。或者学习了设计模式后就开始肆无忌惮地到处用设计模式,就连最简单的打印一个Hello Word的入门程序也都能用上几个设计模式的话,那就是把设计模式当成黄金大锤了。
有一种“面向接口编程”滥用的反模式,就是“一个服务一个接口”。这种常见的就是所有的服务类都有一个所谓的XxxService及XxxServiceImpl,前者是一个接口,后者是对应的惟一实现。问题的关键在于,这种XxxService和XxxServiceImpl竟然一一对应,也就是说一个XxxService其实只有一个XxxServiceImpl与其对应。这如果不是对“面向接口编程”的一种曲解与滥用,那就是“夸夸其谈的未来性(Speculative Generality)”的代码坏味。“接口”在面向对象的设计中,是属于抽象层面的东西,那什么时候需要抽象呢,一定是两种及以上事物拥有一些共同特征时,才能形成抽象(自底向上);或者从高层定义一些抽象特征,由两种或以上事物来体现这个特征,这种抽象才有意义(自顶向下)。比如光喊我一个人吃饭,你根本不需要抽象,喊我的名字我就来了(我的思路是,吃饭不积极肯定有问题),但是我跟很多男的在一起的时候,你喊:“IT男们,走啰”,我们就一起过来了。那么这种“IT男们”就是一种抽象,只当有两个以上的实体的时候,这种抽象才有必要和更有意义。
在数据类型使用方面往往也有类似的锤子问题。比如涉及到List的统统都是ArrayList,涉及到Map那就都是HashMap了,其它类型那就统统String,好像其它的都不存在了一样,这就相当于把String、ArrayList和HashMap当成了处理全部数据类型的黄金大锤了。
我曾见识过一个根据电话号码段查找归属地的实现,堪称经典。原始实现是,将数据库包含起始号码、结束号码以及归属地市几个字段的表中所有记录,按起始号码排序后一次性地读入到程序内存中,然后每次查找特定号码的归属地时,在数据结构中顺序查找比对,使用的数据结构是ArrayList
在阅读代码时,还经常会看到这种函数的入参和返回的类型都是Map,OMG感觉就是一个黑洞,没有任何语义,只知道是一个大麻袋,里面是什么,深不可测。比如下面这个activeVcher函数,短短几行代码中里面就出现了4处Map,我想写这个代码的人一定是丐帮中的四袋弟子。当然这也怪我当初刚入道JAVA时留下的一个“债务”,没想到6年以后,债务依然还在向前滚动。当年我还不知道代码可读性,更不知道POJO的意义,以及语义化的涵义,只是感觉Map好灵活好喜欢。只是今日回头一看,竟然有种在浓浓的雾霾中看不清前路的感觉,看到Map摸不清里面到底兜了些什么,套句流行的话说“雾是String的浓,霾是Map的厚”。
3.总结
入了设计模式的门,又摸了一下反模式的皮毛,到此我们终于大概摸清了模式这头大象比较完整的轮廓了,大家可以尝试骑象远行了。在日常工作中,我们也可以尝试把一些常见的写法进行归纳总结,好的命名成XXX模式,不好的就命名成YYY反模式。只要坚持不懈,最终我们可以超越模式本身,进一步提升认识,跨越到“码可码、非常码,道可道、非常道”的境界之中了。