switch-case代码优化

1、前言

    在实际的编程中,我们经常会使用到switch..case语句,这通常也是对一长串if..else if语句的优化。对于一些简单的情况(只每个case代码中代码长度不会很长,而且case分之并不多的情况),用switch..case语句即可,此时代码的可读性并不会很差,结构也算是清晰。但是一旦case分支数目众多,每个case语句块中代码长度也很长,这时对于维护这段代码的同学则是个噩梦了(本人就遇到过一段代码,case分支有近20个,每个case语句块中,代码长度均有几十上百行,有的甚至有几百行之多。看到这段代码时,我也是醉了,果断对它进行重构)。

    对于switch..case语句有几种常用的方式,下面我一一为大家介绍。在正式介绍之前,我们优化的目标代码如下所示:


   
   
  1. struct Param
  2. {
  3. ... //待传入FuncToOptimal函数的参数
  4. }
  5. enum EDataType
  6. {
  7. Avg,
  8. Min,
  9. Max,
  10. TI,
  11. Tab ,
  12. ... //更多其他的枚举值
  13. };
  14. double FuncToOptimal(EDataType eDataTypeToCompute, const Param& inParam)
  15. {
  16. double dRetValue = 0;
  17. ... //局部变量定义
  18. switch (eDataTypeToCompute)
  19. {
  20. case Avg:
  21. for (...)
  22. {
  23. do
  24. {
  25. ... //(1)Avg情况下的计算内容
  26. } while (...);
  27. }
  28. break;
  29. case Min:
  30. for (...)
  31. {
  32. do
  33. {
  34. ... //(2)Min情况下的计算内容
  35. } while (...);
  36. }
  37. break;
  38. case Max:
  39. for (...)
  40. {
  41. do
  42. {
  43. ... //(3)Max情况下的计算内容
  44. } while (...);
  45. }
  46. break;
  47. case TI:
  48. for (...)
  49. {
  50. do
  51. {
  52. ... //(4)TI情况下的计算内容
  53. } while (...);
  54. }
  55. break;
  56. case Tab:
  57. for (...)
  58. {
  59. do
  60. {
  61. ... //(5)Tab情况下的计算内容
  62. } while (...);
  63. }
  64. break;
  65. case ...: //更多其他的case
  66. ...
  67. break;
  68. default:
  69. for (...)
  70. {
  71. do
  72. {
  73. ... //(6)default情况下的计算内容
  74. } while (...);
  75. }
  76. break;
  77. }
  78. return dRetValue;
  79. }

    以上只是示例代码,在实际中有可能会比这个更为复杂,一个函数就有可能数千行。对于一个对于业务不熟悉的人,看到这样的代码自然是没有兴趣或者是没有勇气继续往下看的,因为这种代码的可读性是非常差的,后期维护成本会很高,在实际的工作中我们应该尽可能的避免这种代码的出现。对于以上示例代码,有以下三种优化方式:抽取法、继承与多态、跳表法,以下分别进行介绍:

 

2、抽取法

    所谓抽取法,即我们将每个case语句中的计算内容抽取为一个函数,然后在每个case中只需要进行函数调用即可,如我们将“Avg”这条分之抽取为如下函数:


   
   
  1. void ComputeAvg(const Param& inParam)
  2. {
  3. double dAvg = 0;
  4. ... //应用于Avg分之的局部变量
  5. ... //实际的计算工作
  6. return dAvg;
  7. }

    然后在FuncToOptimal函数中,每个case分支调用对应的函数即可,那么FuncToOptimal就简化为:


   
   
  1. double FuncToOptimal(EDataType eDataTypeToCompute, const Param& inParam)
  2. {
  3. double dRetValue = 0;
  4. switch (eDataTypeToCompute)
  5. {
  6. case Avg:
  7. dRetValue = ComputeAvg(inParam);
  8. break;
  9. case Min:
  10. dRetValue = ComputeMin(inParam);
  11. break;
  12. case Max:
  13. dRetValue = ComputeMax(inParam);
  14. break;
  15. case TI:
  16. dRetValue = ComputeTi(inParam);
  17. break;
  18. case Tab:
  19. dRetValue = ComputeTab(inParam);
  20. break;
  21. case ...:
  22. ... //其他情况
  23. break;
  24. default:
  25. dRetValue = ComputeDefault(inParam);
  26. break;
  27. }
  28. return dRetValue;
  29. }

    这样最明显的好处就是,FuncToOptimal函数不再冗长,它函数的代码行数会大大缩减,使得代码的可读性增强。并且每个case分之的实现都单独在一个接口中实现,这也更能满足一个函数只做一件事的原则。除此之外,这种方法还避免了在函数入口定义所有case分支所需要的局部变量。

    尽管如此,但是用这种方法重构之后,FuncToOptimal的函数实现仍然较长,尤其是当EDataType枚举有十几个甚至几十个枚举值的情况。另外,这个函数也违背了一个函数只做一件事的原则(严格来讲,有多少种case分支,它就做了多少件事情)。

 

3、跳表法

    经过抽取法优化之后,虽然函数的可读性增强了,但是函数依然违背了单一原则以及对修改封闭的原则,因为当增加新的枚举值时,我们仍然需要来对函数进行修改。

    为继续修改代码,使之满足单一原则和对修改封闭原则,我们可以使用 跳表法,它需要结合之前所讲的抽取法,首先将每个case分支的实现抽取到单独的函数中,然后用一个数组来存储对应的case分支实现的函数地址,这个数组即为跳表。如:


   
   
  1. typedef double (*DataRetriver)(const Param& inParam); //定义函数指针
  2. DataRetirver dataRetriever[ 20] = {&ComputeAvg, &ComputeMin, ...}; //假设总共有20个分支

    定义好跳表之后,对FuncToOptimal的优化如下:     


   
   
  1. double FuncToOptimal(EDataType eDataToCompute, const Param& inParam)
  2. {
  3. double dTabValue = 0;
  4. DataRetriever * pRetriever = dataRetriever[eDataToCompute];
  5. if(pRetriever != nullptr)
  6. {
  7. dTabValue = pRetriever(inParam);
  8. }
  9. return dTabValue;
  10. }

    经过优化之后,FuncToOptimal就同时满足了单一原则和对修改封闭原则。但是跳表法有几个明显的缺点:

    1)跳表中,每一个位置的函数指针必须与枚举值严格对应;

    2)当枚举值调整之后,跳表必须做相应的调整;

    3)对于有些不用实现的case分支,跳表中也必须要为其对应的枚举值留空位

 

4、继承与多态

    所谓继承与多态,即利用C++中的多态性质。优化主要有以下几个步骤:

    1)创建一个基类


   
   
  1. class DataRetriever
  2. {
  3. public:
  4. DataRetriver() = default;
  5. ~DataRetirver() = default;
  6. double RetrieveData(const Param& inParam) = 0;
  7. }

    2)每一种需要计算的case分支均创建一个类,继承至DataRetriever,如Avg分支:


   
   
  1. class AvgRetriever : public DataRetirever
  2. {
  3. public:
  4. AvgRetriever() = default
  5. ~AvgRetriever() = default;
  6. double RetrieveData(const Param& inParam);
  7. }

    3)实现RetrieveData接口,如Avg分支的实现:


   
   
  1. double AvgRetirever::RetrieveData(const Param& inParam)
  2. {
  3. double dAvg = 0;
  4. ... //应用于Avg分之的局部变量
  5. ... //实际的计算工作
  6. return dAvg;
  7. }

    4)创建一个工厂方法(当然也可以作为DataRetriever的静态函数方法,此处仅为示例之用),根据不同的枚举值获取相应的对象


   
   
  1. DataRetriever* GetRetriever(EDataType eDataTypeToCompute)
  2. {
  3. switch(eDataTypeToCompute)
  4. {
  5. case Avg:
  6. return new AvgRetriever();
  7. case ...:
  8. return new ...; //其它情况
  9. }
  10. return nullptr;
  11. }

    注:返回值此处直接返回指针,仅仅为示例之用,实际工作中尽量使用智能指针

    5)对FuncToOptimal进行优化,优化后函数实现变为:


   
   
  1. double FuncToOptimal(EDataType eDataToCompute, const Param& inParam)
  2. {
  3. double dTabValue = 0;
  4. DataRetriever * pRetriever = GetRetriever(eDataToCompute);
  5. if(pRetriever != nullptr)
  6. {
  7. dTabValue = pRetriever->RetrieveData(inParam);
  8. delete pRetriever;
  9. pRetirever = nullptr;
  10. }
  11. return dTabValue;
  12. }

    经过以上5个步骤即可完成对FuncToOptimal函数的重构,重构后函数会缩短到几行。并且代码的结构清晰,可读性强。当任何一个分支的实现改变,或者新增了新的分支,我们都无需对FuncToOptimal进行修改,只需要修改对应分支的类或者为新的分支实现一个类,并在GetRetriever工厂方法中增加对新的分支的处理即可。

 

5、总结

    在本为中,我们针对switch...case语句的优化给出了以下3中解决方法:

    1)抽取法

    这种方法最为简单,它能够有效的减少代码行数,并增强代码可读性,降低维护成本。但这种方法后,函数仍然不符合单一原则以及对修改封闭原则,并且函数对修改不封闭。

    2)跳表法

    这种方法是在抽取法之上对函数进行进一步的优化。它能够使得优化后的代码满足单一原则和对修改封闭原则,但它不够灵活。

    3)继承与多态

    这种方法是三种方法中,对函数优化最为彻底的方法,对于复杂的switch...case语句,我们通常建议使用这种方法。

    最后,并不是所有的switch...case语句都需要优化,只有case分支较多,且分支中处理比较复杂的情况才需要进行优化。在优化的时候,也不是一定要选择继承与多态的方法,这需要根据具体情况而定。

    P.S. 以上为个人意见,如有纰漏,请您不吝赐教。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值