上一篇:《把设计模式用起来(2)——用不好?之实践不足》
本篇继续讲设计模式用不好的常见原因,这是第二个:使用设计模式的时机不对。
二、时机不对
这里说的时机并不是单纯指软件研发周期中的时间阶段,而是指条件;更具体一点,指程序员应该满足什么条件,才是使用设计模式的好时机。
第一个条件前面说过了,得有问题。第二个条件,程序员需具备分析问题的能力。毕竟,只有通过分析才能从问题得到模式,如图:
图: 从问题到模式
这张图中最重要的是分析,最不重要的是模式。你甚至可以认为,一个没学过模式的程序员面对某个具体问题,只要分析得当,往往也能应用正确的模式,只是他自己不知道自己组织代码方式,被称作某某模式。
那么,我们要如何获得更强的分析能力呢?其一,对问题所属的业务逻辑越熟悉,对问题就越了解;其二,既然模式是此过程的结果,那么,熟悉模式也有利于我们强化分析问题的能力。由此,我们可以画一张更复杂一点的图:
图: 问题-分析-模式
尽管熟悉模式倒过来有助于理解问题,但程序员的更多努力仍需用在正向路径:熟悉业务→理解问题→分析问题→找到并应用正确的设计模式。
在理解问题的基础上,思考是否需要以及如何使用设计模式,这个结论听起来是如此的自然而然,几乎称得上是一条“公理”,但确实有很多程序员,特别是正在学习设计模式或自诩擅长设计模式的程序员,容易违背,犯“先有模式,然后到处找问题”的错误。
有同学说,“老师,虽然我心里确实装满设计模式,快溢出来的那种,但我并不会犯拿模式硬套问题的低级错误;相反,我总是在看到某个问题和某个模式匹配度很高的情况后,才会放心地套用设计模式。”
“只是放心吗?难道没有开心?”
“当然也开心啊!每当用对一次设计模式,我的内心充满成就感。”
这正是罪之所在:对设计模式的使用,有迫切的期待。这是贪欲,这是心魔,是引发更多软件研发问题的万恶之源。这种心态很容易让我们提倡的“工作逼迫你使用设计模式”,变成“你逼迫工作使用设计模式”。
说这么多,是在表达一个观点:使用设计模式应宁缺勿滥,因为,一段用错设计模式的代码往往比未使用模式的代码,更令后来人头痛。
每一个设计模式,都是在表达特定意图(Intent)的一种组织代码的方式。一段代码用上一个设计模式,意味着这段代码至少多出了两个明显的属性:一是它的意图,二是它的代码结构。一段什么设计模式都没用上代码,它就是一段代码;而一段代码用上了设计模式,代码中的设计模式就像人群有个显眼包,它会一直叫嚷:“看我,看我!我的设计意图是……我的实现结构是……”
那么,阅读者接收到这两个信息,是好事还是坏事?这就得看设计模式用对与否。幸福的代码都是相似的,不幸的代码却各有各的不幸,见表:
功能满足 (业务) | 模式选择 (意图) | 模式实现 (结构) | 不幸的效果 |
ꭗ | ꭗ | ꭗ | 表达了错误的意图且该意图未正确实现,程序不对劲 |
ꭗ | ꭗ | √ | 表达并实现了一个错误的意图,程序不对劲 |
ꭗ | √ | ꭗ | 意图正确,但该意图实现有错,程序不对劲 |
√ | ꭗ | ꭗ | 表达了错误的意图,且意图实现有误,但程序能跑对 |
√ | ꭗ | √ | 用一个错误的模式实现了程序所需的功能 |
无论模式选对选错,也无论功能满足与否,表中几种应用情况中的设计模式,都会给代码阅读者带来更多干扰甚至误导,毕竟它们会说话。
这里特别讲讲表中最后一类不幸:“用一个错误的模式实现了程序所需功能”。这里最常见的“错误”,就是用了一个并不需要的模式。比如,使用策略模式,但直至系统下线也没用上第二个策略。再如,实现了一个无比强大的解释器,却只用它顺序执行指令。
丁小明严肃地从座位站起来:“老师,您应知道‘防御性编程’?程序员总是预设程序存在问题且将持续需要修改。所以,尽管在刚开始做设计时我们无法预测是否有第二个策略,但我们应该预设它有。至于解释器的例子,确实有点过度设计之嫌,但原则上,假定未来会有复杂的流程脚本需要执行,不对吗?”
这是很多同学都会有的疑惑:防御性编程是对的,可是切忌过度设计也是对的,日常编程做设计,能不能有一个简单明了且有效的方法,能迅速将我们从“加上!”和“不要!”,甚至是“砍掉!”的纠结中拉出来?
具体到何时使用设计模式这个问题,新的疑惑是:假设我还没有完全掌握业务需求就被迫写代码,那这些代码未来一定会变,那么,我现在是不是应该预防性地多加使用设计模式?毕竟,设计模式生来就是为了应对变化。让我们画张图来更清楚的表达:
因为会有变化,所以就马上用模式?
这种想法是错的,它把工作理想化了,并且弄错了工作首要目标。编程工作首先需保证正确实现当下的功能,然后才去考虑如何更好地就应对未来的变化。
为什么是这样先后次序?有两个理由,第一个略显功利:先快点做对,再慢慢做好,这样比较不会挨训,也可以反过来说,你上来就花大力气让代码能应对未来的65536种变化,大概率也得不到领导夸奖。第二个理由很客观:后者依赖前者,即,通常,正是在从不对,到不那么对,到最终做对的过程中,才能对眼下的功能、未来的变化、应有的设计这三者都产生深刻的理解。
丁小明再次发问:“有些功能未来会发生的变化,在程序员一行代码未写时,就可以猜出十之八群,这种情况下,程序员直接用上设计模式,也不合理吗?”这种理想状态当然存在。诸如:
- 项目前期工作非常棒,需求清晰,设计详细,详细到此处该用什么模式都写出来了;
- 你对此类问题有深刻理解,或有可靠的经验,此类功能后续的变化路径,你看得清楚、也看得长远;
- 针对眼前这类问题,业界有教科书般的解决方法。
更多的时候,程序员处在水深火热中:
- 甲方对现有的功能都说不清楚,遑论未来的变化;
- 甲方基于防御性心理,罗列了各种各样的,未来可能的变化;
- 甲方没怎么提,我方(领导、产品经理、程序员自己)设想了一大堆;
- 甲方没怎么提,我方也没怎么想,但因为业务领域问题,我方并未正确理解甲方的真实需求;
- 甲方提了一些,我方想了一些,并且我方正确地理解了甲方,只是不管甲方说还是我方想的,都有不对的地方;
- ……
如果某一块的功能需求还比较模糊,作为乙方,千万不要自信可以通过一个“强大的设计”以做到“以不变应万变”,从而“立于不败之地”。此时的正确做法,应是直接的、快速地把当前所理解的功能做出来,并借助它尽快确定相关需求。
对问题的理解是一切设计的基础,不过,实现设计的过程,能极大帮助程序员全面地,深入地理解问题。多数时候,甲方能告诉你需求,但不能帮你做需求分析,而程序员因为跨领域的原因,所做的需求分析仍然有不少模糊与暧昧。比如,功能F1客观上需要的输入是I1、I2、I3,但程序员有可能在开始时认为是I1、I3和I4。有很多方法有助发现并纠正这个错误认知,其中一种非常高效的方法,就是编写代码尝试实现,在实现的过程,即可验证、强化、纠偏、补充程序员对业务系统的理解,包括厘清对象职责和理清对象关系,这二者是面向对象和用对设计模式的重要基础。
这是本小节的结论:除非你对问题理解到位透彻,否则不要一开始写代码就想着使用设计模式,在功能写对之前,更不要为了该功能“未来可能的变化”去套用设计模式。
这个结论只说了不要,那什么时候可以要呢?两点:
台下有同学一脸落莫,问:“老师,我赞同第二点,但我们的领导不愿意给我重构所需的资源,比如时间,怎么办?”。这是回到上一小节“实践不足”中的情况B了。你可以屈服,从此不去想设计模式的好;也可以造反,说服领导,或想办法让自己当上领导;还也可以跳槽。无论如何,我都不建议一个程序员在这种恶劣的工作环境下,一边要抓紧完成业务功能,一边要努力用对设计模式。注意,当我说这话时,我站边技术主管。本来,主管只需检查你功能做对做错,现在,他需要同时检查功能和模式,其中后者还有需区分选择和实现上的错误,诸如:(a) 功能做错,模式选错,模式写错;(b) 功能做错,模式选对,模式写错; (c) 功能做对,模式选错,模式写对……(参见表2)。
猜,技术主管在这种场景下,最喜欢说的是哪一句话?答:“你这是要把错误雕成一朵花吗?”
来听一个源于真实案例改编的故事吧。主人公本应是丁小明,为节省篇幅,我们用“你”来代替。
假设问题为Q,正确答案是A;而因为理解有误,你以为需要实现的业务逻辑是B,并且你迅速想到B未来有可能发展成B1、B2、B3。想到这里,你一边啜吸奶茶发出嗞嗞嗞的声音;一边在脑海中构建原始思路C。为了更好地表达思路,也为了更好地应对B1、B2、B3,你果断选用设计模式D,D的意图是I。不过,由于你还是设计模式新人,所以你真实想表达的意图其实是J,而你写的模式很像D但又不是D,称作“D' ”。测试一把,确实能实现B,你充满成就感地提交了代码E。
技术主管开始审查E,他迅速嗅到D的味道;会心一笑,品一口咖啡,感觉自己像羽扇纶巾的诸葛亮,准备欣赏一场漂亮的战役,看看自己的员工如何通过设计模式优雅地干掉Q。10分钟过去,20分钟过去,30分钟过去……经验老到的主管,终于发现D` 和 D 只是长得像而已。
主管放下代码,翻出上次设计模式内训的文档,发现当时你写的答案那叫一个似是而非。他叹一口气,陷入自责。
1个小时过去,主管猛然领悟,你的真实意图是J。
2个小时过去,主管醍醐灌顶:“这家伙想要实现的业务逻辑是B,不是A!”,不过,纵使如此,也不应该用D啊?看一眼时间是夜里11点30分,还早;他拨通了你的电话。
夜深人静,主管的身边人呼吸均匀,偶尔吐一两句梦话。主管轻悄悄地换个姿式,继续听话里的你兴奋地谈着:“B1、B2、B3变化,可能还是保守了,我有一种预感,三个月以后,随着用户量的剧增,B99,B100,B101也是有可能发生的……”
这样的破事来个三四次,故事中主人公的工作类型就会发生变化,参见上一小节“实践不足”的情况D、情况C、情况A。