2018-10-18 重构的那些事儿-令人厌恶的If~else switch case

原文出处: Jun.M

几天前的一次上线,脑残手抖不小心写了bug,虽然组里的老大没有说什么,但心里面很是难过。同事说我之所以写虫子是因为我讨厌if/else,这个习惯不好。的确,if/else可以帮助我们很方便的写出流程控制代码,简洁明了,这个条件做什么,那个条件做什么,说得很清楚。说真的,我从来不反对if/else,从经验上看,越复杂的业务场景下,代码写的越简单单一,通常越不容易出错。以结果为导向的现代项目管理方式,这是一种很有效实践经验。

同事说的没错,我的确很讨厌if/else。这个习惯很大程度是受Thoughtworks一位咨询师朋友影响,他经常在我耳边唠叨,写代码要干净,要简洁,要灵活多变,不要固守城规,不要动不动就if/else,switch/case。初入IT领域,我一直把这句话奉为经典。在以后的学习工作中也时刻提醒自己要让自己的代码尽可能的看起来简洁,不失灵活。不喜欢if/else并不意味着拒绝它,该使用的时候必要使用,比如函数接口入参check,处理异常分支逻辑流程等。通常能不用分支语句,我尽量不会使用,因为我觉得if/else很丑,每每看到if/else代码,总会以挑剔的眼光看待它,想想能不能重构的更好。大多数时候,关于什么好的代码,大家的意见往往分歧很大,每个人都有各自的想法,审查你代码的人可能会选择另一种实现方式,这并不能说明谁对谁错。

OO设计遵循SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)原则,使用这个原则去审视if/else,可能会发现很多问题,比如不符合单一原则,它本身就像一团浆糊,融合了各种作料,黏糊糊的很不干净;比如不符合开闭原则,每新增一种场景,就需要修改源文件增加一条分支语句,业务逻辑复杂些若有1000种场景就得有1000个分支流,这种情况下代码不仅仅恶心问题了,效率上也存在很大问题。由此可见,if/else虽然简单方便,但不恰当的使用会给编码代码带来非常痛苦的体验。针对这种恶心的if/else分支,我们当然首先想到的去重构它–在不改变代码外部功能特征的前提下对代码内部逻辑进行调整和优化,但,如何做呢?前段时间在项目中正好遇到一个恶心的if/else例子,想在这篇博客里和大家分享一下去除if/else重构的历程。

image

if/else的恶瘤

有句话说的好–好文章是改出来,同样,好的代码也肯定是重构出来的,因为没有哪个软件工程师能够拍着胸脯保证在项目之初代码设计这块,就考虑到了所有需求变化可能性的扩展。随着项目的不断成长,业务逻辑变的越来越复杂,代码也开始变的越来越多,原有的设计可能不再满足需求,那么此时必须要重构。就系统整体架构而言,重构可能需要很大的改动,可能在架构流程上需要评审;就功能内代码层次而言,这种重构在我们编码过程中随时可以进行,类似于if/else,swicth/case这种代码的重构也属于这种类型。今天我们要重构的if/else源码如下所示,针对不同的status code,CountRecoder对象会执行不同的set方法,为不同内部属性赋值。

Java


public  CountRecoder getCountRecoder(List countEntries)  {

    CountRecoder countRecoder  =  new  CountRecoder();

    for  (CountEntry countEntry  :  countEntries)  {

        if  (1  ==  countEntry.getCode())  {

            countRecoder.setCountOfFirstStage(countEntry.getCount());

        }  else  if  (2  ==  countEntry.getCode())  {

            countRecoder.setCountOfSecondStage(countEntry.getCount());

        }  else  if  (3  ==  countEntry.getCode())  {

            countRecoder.setCountOfThirdtage(countEntry.getCount());

        }  else  if  (4  ==  countEntry.getCode())  {

            countRecoder.setCountOfForthtage(countEntry.getCount());

        }  else  if  (5  ==  countEntry.getCode())  {

            countRecoder.setCountOfFirthStage(countEntry.getCount());

        }  else  if  (6  ==  countEntry.getCode())  {

            countRecoder.setCountOfSixthStage(countEntry.getCount());

        }

    }

    return  countRecoder;

}

CountRecoder对象是一个简单的Java Bean,用于保存一天之中六种状态分别对应的数据条目,提供了get和set方法。CountEntry是对应数据库中每种状态的数据条目记录,包含状态code和以及count两个字段, 我们可以使用mybatis实现数据库记录和java对象之间的转换。上面getCountRecoder的方法实现了将list转换为CountRecoder的功能。

看到这段代码,想必已经有很多人要呵呵了,像一坨啥啥啥,长得这么丑,真不知道它”爸妈”怎么想的,怎么敢”生”出来。啥都不说了,直接回炉重构吧。重构是门艺术,Martin flow曾写过一本书《重构改变代码之道》,里面详细的记录了重构的方法论,感兴趣的朋友可以阅读一下。说到重构,通常我们在重构中会遇到一个问题,那就是如何能够保证重构的代码不改变原有的外部功能特征 ?经过TDD训练的朋友应该知道答案,那就是单元测试,重构之前要写单元测试,准确的来说应该是补单元测试,毕竟TDD的核心理念是测试驱动开发。对于今天博客中分享的例子,因为代码逻辑比较简单,所以偷了懒,省却了单元测试的历程。

重构初体验–反射

要重构上面的代码,对设计模式精通的人可以立马可以看出来这是使用策略模式/状态模式的绝佳场景,将策略模式稍微变换,工厂模式应该也是ok的,当然也有些人会选择使用反射。对于这些方法,这里不一一列出,主要想讲一下使用反射和工厂模式如何解决消除if/else问题,那先说反射吧,代码如下所示:

Java


private  static  Map methodsMap  =  new  HashMap();

static  {

    methodsMap.put(1,  "setCountOfFirstStage");

    methodsMap.put(2,  "setCountOfSecondStage");

    methodsMap.put(3,  "setCountOfThirdtage");

    methodsMap.put(4,  "setCountOfForthtage");

    methodsMap.put(5,  "setCountOfFirthStage");

    methodsMap.put(6,  "setCountOfSixthStage");

}

public  CountRecoder getCountRecoderByReflect(List countEntries)  {

    CountRecoder countRecoder  =  new  CountRecoder();

    countEntries.stream().forEach(countEntry  ->  fillCount(countRecoder,  countEntry));

    return  countRecoder;

}

private  void  fillCount(CountRecoder shippingOrderCountDto,  CountEntry countEntry)  {

    String  name  =  methodsMap.get(countEntry.getCode());

    try  {

        Method declaredMethod  =  CountRecoder.class.getMethod(name,  Integer.class);

        declaredMethod.invoke(shippingOrderCountDto,  countEntry.getCount());

    }  catch  (Exception  e)  {

        System.out.println(e);

    }

}

重构初体验–所谓模式

使用反射去掉if/else的原理很简单,使用HashMap建立状态码和需要调用的方法的方法名之间的映射关系,对于每个CountEntry,首先取出状态码,然后根据状态码获得相应的要调用方法的方法名,然后使用java的反射机制就可以实现对应方法的调用了。本例中使用反射的确可以帮助我们完美的去掉if/else的身影,但是,众所周知,反射效率很低,在高并发的条件下,反射绝对不是一个良好的选择。除去反射这种方法,能想到的就剩下使用策略模式或者与其类似的状态模式,以及工厂模式了,我们以工厂模式为例,经典的架构UML架构图通常由三个组成要素:

  1. 抽象产品角色:通常是一个抽象类或者接口,里面定义了抽象方法
  2. 具体产品角色:具体产品的实现类,继承或是实现抽象策略类,通常由一个或多个组成类组成。
  3. 工厂角色:持有抽象产品类的引用,负责动态运行时产品的选择和构建

策略模式的架构图和工厂模式非常类似,不过在策略模式里执行的对象不叫产品,叫策略。在本例中,这里的产品是虚拟产品,它是服务类性质的接口或者实现。Ok,按照工厂模式的思路重构我们的代码,我们首先定义一个抽象产品接口FillCountService,里面定义产品的行为方法fillCount,代码如下所示:

Java


public  interface  FillCountService  {

    void  fillCount(CountRecoder countRecoder,  int  count);

}

接着我们需要分别实现这六种服务类型的产品,在每种产品中封装不同的服务算法,具体的代码如下所示:

Java


class  FirstStageService  implements  FillCountService  {

    @Override

    public  void  fillCount(CountRecoder countRecoder,  int  count)  {

        countRecoder.setCountOfFirstStage(count);

    }

}

class  SecondStageService  implements  FillCountService  {

    @Override

    public  void  fillCount(CountRecoder countRecoder,  int  count)  {

        countRecoder.setCountOfSecondStage(count);

    }

}

class  ThirdStageService  implements  FillCountService  {

    @Override

    public  void  fillCount(CountRecoder countRecoder,  int  count)  {

        countRecoder.setCountOfThirdtage(count);

    }

}

class  ForthStageService  implements  FillCountService  {

    @Override

    public  void  fillCount(CountRecoder countRecoder,  int  count)  {

        countRecoder.setCountOfForthtage(count);

    }

}

class  FirthStageService  implements  FillCountService  {

    @Override

    public  void  fillCount(CountRecoder countRecoder,  int  count)  {

        countRecoder.setCountOfFirthStage(count);

    }

}

class  SixthStageService  implements  FillCountService  {

    @Override

    public  void  fillCount(CountRecoder countRecoder,  int  count)  {

        countRecoder.setCountOfSixthStage(count);

    }

}

紧接着,我们需要是实现工厂角色,在工厂内需要实现产品的动态选择算法,使用HashMap维护状态code和具体产品的对象之间的映射关系,就可以非常容易的实现这一点,具体代码如下所示:

Java


public  class  FillCountServieFactory  {

    private  static  Map fillCountServiceMap  =  new  HashMap();

    static  {

        fillCountServiceMap.put(1,  new  FirstStageService());

        fillCountServiceMap.put(2,  new  SecondStageService());

        fillCountServiceMap.put(3,  new  ThirdStageService());

        fillCountServiceMap.put(4,  new  ForthStageService());

        fillCountServiceMap.put(5,  new  FirthStageService());

        fillCountServiceMap.put(6,  new  SixthStageService());

    }

    public  static  FillCountService getFillCountStrategy(int  statusCode)  {

        return  fillCountServiceMap.get(statusCode);

    }

}

客户端在具体使用的时候就变的很简单,那getCountRecoder方法就可以用下面的代码实现:

Java

public  CountRecoder getCountRecoder(List countEntries)  {

    CountRecoder countRecoder  =  new  CountRecoder();

    countEntries.stream().forEach(countEntry  ->

            FillCountServieFactory.getFillCountStrategy(countEntry.getCode())

                    .fillCount(countRecoder,  countEntry.getCount()));

    return  countRecoder;

}

重构初体验–Java8对模式设计的精简

和反射一样使用设计模式也同样完美的去除了if/else,但是不得不引入大量的具体服务实现类,同时程序中出现大量的模板代码,使得我们程序看起来很不干净,幸好Java 8之后引入了Functional Interface,我们可以使用lambda表达式来去除这些模板代码。将一个接口变为Functional interface,可以通过在接口上添加FunctionalInterface注解实现,代码如下所示:

Java


@FunctionalInterface

public  interface  FillCountService  {

    void  fillCount(CountRecoder countRecoder,  int  count);

}

那么具体的服务实现类就可以使用一个简单的lambda表达式代替,原先的FirstStageService类对象就可以使用下面的表达式代替:

Java


(countRecoder,  count)  ->  countRecoder.setCountOfFirstStage(count)

那么工厂类中的代码就可以变为:

Java


public  class  FillCountServieFactory  {

    private  static  Map fillCountServiceMap  =  new  HashMap();

    static  {

        fillCountServiceMap.put(1,  (countRecoder,  count)  ->  countRecoder.setCountOfFirstStage(count));

        fillCountServiceMap.put(2,  (countRecoder,  count)  ->  countRecoder.setCountOfSecondStage(count));

        fillCountServiceMap.put(3,  (countRecoder,  count)  ->  countRecoder.setCountOfThirdtage(count));

        fillCountServiceMap.put(4,  (countRecoder,  count)  ->  countRecoder.setCountOfForthtage(count));

        fillCountServiceMap.put(5,  (countRecoder,  count)  ->  countRecoder.setCountOfFirthStage(count));

        fillCountServiceMap.put(6,  (countRecoder,  count)  ->  countRecoder.setCountOfSixthStage(count));

    }

    public  static  FillCountService getFillCountStrategy(int  statusCode)  {

        return  fillCountServiceMap.get(statusCode);

    }

}

这样我们的代码就重构完毕了,当然了还是有些不完美,程序中的魔法数字不利于阅读理解,可以使用易读的常量标识它们,在这里就不做过多说明了。

总结

Craig Larman曾经说过软件开发最重要的设计工具不是什么技术,而是一颗在设计原则方面训练有素的头脑。重构的最终结果不一定会让代码变少,相反还有可能增加程序的复杂度和抽象性,就本例中的if/else而言,确实如此。我非常赞同我的一位朋友说的话,做技术要有追求,没错if/else可以在代码中工作的挺好,也可以很容易的被接替者所理解,但是我们可以有更好的选择,因为简单的代码也可以变得很精彩。多勤多思,也许有一天真的就可以达到Craig所说的在设计原则方面拥有训练有素的头脑,谁说不是这样呢?加油吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容