最近我工作的主要内容,是在和别人结对编程,以对一个大型的遗留系统项目进行重构。
过程中,我发现一个特别有意思的东西,我重构了很多的 if 语句。从这些 if 语句里,大抵是映射出了业务的变化。于是,我便想写一篇文章来记录一下相关的心得。
你写的 if 就是你的犯罪证据
业务的复杂性,导致了架构的复杂性。在这些代码故事里,发生得最多的地方就是 if 语句。所以,你可以从大部分的 if 语句里,看到一些代码上的坏味道。
业务条件复杂
你先写了一个 if 语句里面只有一个条件,没问题。但是后来的人,又加了一个条件,因为业务上确实需要这么做。于是,后来,又不得加了一个if 语句,导致了这个条件变得更加复杂。
- if(isCondition && isNotASwitchCase && .... && ....) {
- }
所以,完了,这些代码越来越难以维护。
于是,我们应对于这类条件判断,有两种做法:提取变量和提取方法。当你的判断条件是一个方法的时候,你可以想象一下它的架构是多么的复杂。
难以阅读的字符串判断
开始的人加了一个简单的条件判断,因为当时真的只有这么一种业务场景。你又不能过度设计,成一个 switch-case。但是,后来又多了好多个场景。
- if(aCondition =="A") {
- } elseif(bCondition =="B") {
- }
更不要提有人在每个 if 里写一个: if (myString.toUpperCase.equals(myOtherString.toUpperCase))。
针对于有限的 if 语句来说,可以转为 switch case(在 IDEA 里只需要 alt + enter 就可以自动完成)。
随着时间的推移我们的条件越来越复杂,我们的 if 语句会越来越复杂。
多层嵌套 if 语句
随着 if 条件进一步扩大化,我们的条件语句就变成了一个多层嵌套的循环语句。每多一层嵌套代码复杂度就 * 2,它的阅读难度就越来越大。于是乎:
- if(condition) {
- if(blabla) {
- ...
- }
- }
面对这一类 if 条件语句,我们能所做的就是:
- 提供方法
- 反转 if 语句
诸如于:
- if(!condition) {return}; // 为了演示方便
- if(blabla){...}
又或者是诸如于三元表达式,不过我讨厌难以阅读的三元表达式——但是,只是 true 和 false 的情况下,还是相当不错的。
复杂的 if 块内逻辑
当业务进一步复杂化的时候,我们的 if 条件里就充斥着各种各样的逻辑。
- if(conditionA) {
- blablaA;
- blaA(blabla).blabla;
- }
我们的 if 方法随之变得越来越长,于是尝试去抽成一个方法。但是,当你又遇到一个新的场景时,你又加了一个 if 语句。后来,又又加了一个 if 语句。你才发现说,『咦,不对,这些 If 语句违反了开闭原则』。
于是,你尝试把代码重构成多态以替换 if 语句。
你开心的话,还可以转为 Factory + Strategy。
你开心的话,你也可以将它转为 HashMap 。
但是,在你写下第一个 if 的时候,你并不知道它会变成什么样的。所以,不要提前去把它转为这么复杂的架构。
上帝 if
如果你的业务场景真的超级复杂,那么你可能会看到一个非常长的 if 代码。它可能有几十个条件,有几百行到几千行的规模。
那么,你可以尝试使用注册表模式+ 注解,通过反射的方式来重构你的 if 语句。
重构
在你进一步修改代码之前,让我们来又双叕明确一下什么叫重构
重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
换句话来说,重构只是在改善现有的代码,使其更易于阅读,换句话来说就是:Clean Code。而当我们说整洁的代码(Clean Code),说的是易于理解、修改和测试的。易于理解和修改意味着:
- 易于理解整个系统的架构
- 易于理解整个应用程序的执行流程
- 易于理解不同对象如何相互协作
- 易于理解理解每种方法的作用
- 易于理解每个表达式和变量的目的是什么
而易于理解的前提便是能让每个团队成员快速理解。(PS:当然了,若是有些人智商不够或者经验不够,他/她需要去需要去增强这方面的能力)。这便意味着,出于这样的目的,你不能编写过于抽象、简练的逻辑。而你又不能写得过于繁琐,充满大量地无用字符。
若是想使代码易于测试,则要先使代码可测试。而在这没有测试之前,我们是难以对代码进行大规模重构。所以,我们就陷入了一个死循环,没有测试,测试不了,没法重构。
WHY
等等,那我们为什么要进行重构呢?为了 ¥¥¥¥¥¥¥$$$$$$$$$ => 快速发布软件。
当软件是一个产品而不是一个项目的时候,我们就需要不断发布新功能,以满足客户的要求。而为了快速发布应用,我们需要让每次的改动最小,测试最少,才能实现快速发布。基于这样一个目标,我们会发现我们的诸多实践都是以此为出发点的。比如说,我们采用插件化、微服务化、组件化的方式,都是为了将软件的改动变小,这样一来,就减少了相应部分的测试工作,从某种意义上来说,就加快了软件发布的流程,从而更好的实现业务价值。因此,我们的第一步就是使二进制改动最小。而要做到二进制改动最小,那么我们就要做到高内聚、低耦合。
因此,不论是在编程还是在设计架构的时候,我们都要尽量满足 SOLID 五项原则中的:
- 单一职责原则:它规定一个类应该只有一个发生变化的原因。
- 开闭原则:软件中的对象应该对于扩展是开放的,但是对于修改是封闭的。
回到问题上
既然,我们都已经知道了,如何去重构,如何用设计模式来解决问题。那么,我们会让我们的代码变得更好吗?不会,因为在流水线式的生产里,每个人都能找到合理的理由。
我们日常开发的模式是:红-绿-重构。而因为时间的原因,我们少去了重构这一步。
上吊绳驱动开发
在上吊绳(deadline)的驱动下,我写了一这篇文章。尽管预先写好了文章的大纲,但是有很多字是打错的。
而对于真实的业务开发来说,要事先设计好相关功能的架构,意味着你得有充足的时间。这样一来说,你在大的方面上才不会犯错。可是呢,你真的有那么多的时间可以设计吗?你今天加的班,还好吗?
代码所有权
改动了你的代码,我就要负责。所以,我不去修改别人的代码。
惧怕修改
没有测试,难以理解代码背后的业务原因。外加之组织文化,导致的沟通障碍;又或者是大家都很忙,没人愿意解释/回顾一下这一块的代码。
能力不够
对,大部分的问题本质都是人的问题。
因为你只需要按下 IDEA 的快捷键,就能完成上面的大部分重构工作。当然了,需要有技巧的按,而不是像 Monkey 一样弹钢琴。
结论
开心就好。