暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

软件开发中多层次if/case的优化技巧

码农的修炼之道 2021-12-22
1115

      在日常开发中,经常看到if里面嵌套if的语句。有一次和资深的码农交流,大部分码农都是写业务代码,只要会if-else即可。他说未必这样,当有大量的if-else嵌套时,能看出不同的码农水平。

  • 大量的if-else嵌套,阅读起来比较难懂,而且找bug都要顺着if的逻辑去分析,太累了。

  • 太多层次的if-else嵌套,需要了解每一层的业务关系,最后才能到达最核心的代码处。

if(conditionA())
{
if(conditionB())
{
func();
}
}

     上面的代码比较简单,可以优化成这样。

//上面的这种代码可以优化如下:
if(conditionA() && conditionB())
   func();

     因为这样的优化,如果conditionA不满足,也不会进入后面的函数conditionB中,效果是一样的,但是确实减少了if的嵌套层次,阅读性更好。上面的案例比较简单,从代码的整洁角度还说不出太大问题,那么我们看一个更复杂一些的例子,在实际开发中优化的案例。

if(conditionA())
{
if(conditionB())
{
if(confitionC())
{
funB();
}
}
else
funC();
}
//这样的案例可以优化如下:
if(!conditionA())
return ;
if(!conditionB())
{
funC();
return;
}
if(conditionC())
   funB();

      上面这几种代码的重构方式叫 Guard Clauses,是一种非常好用的代码重构技巧。这种重构优化的方式尽可能的让出错的先返回, 这样后面就会得到干净的代码。另外,为了提高if语句的分支预测的准确性,我们可以使用likely和unlikely语句,我们看下面的例子说明一下。

if(nullptr == pManager)
return ;
else
funA();

     在上面的案例中,我们对指针对象进行非nullptr的判断,只是增加代码的鲁棒性。实际上大部分时候是不会为nullptr的,也就是if条件是大概率不满足的,那么我们可以提高编译器对分支预测的效果,我们可以优化如下:

if(unlikely(nullptr == pManager))
return ;
else
funA();
//或者使用likely提升分支预测的准确性
if(likely(nullptr != pManager))
   funA();
else
return;

     另外,在我们的日常开发中,还有很多对状态和返回值的判断,会涉及到大量的if语句,这样的代码读起来比较绕,对代码维护也不是很友好,我们可以对功能进行划分,然后包装成函数进行优化,如下面示例代码,需要完成对象的创建及初始化功能,当初始化出错,及时返回结果。

int Init()
{
int rc = 0;
if(nullptr == pContext)
pContext = new Context();
if(nullptr == pContext)
return -1;
rc = pContext->Init();
if(rc < 0)
return rc ;
if(nullptr == pManager)
       pManager = new CManager();
if(nullptr == pManager)
       return -2;
rc = pManager->Init();
if(rc < 0)
return rc;
return 0;
}

       上面的代码干了两件事情,初始化Context对象,如果为空,则创建对象并初始化。其次判断CManager对象是否为空,如果为空则创建对象并进行初始化操作,最终返回0 代表初始化成功,否则初始化失败。针对这种代码,我们可以根据功能拆分为两个函数,分别为Init_Ctx()和Init_Manager();参考代码如下:

int Init_Ctx()
{
if(nullptr == pContext)
      pContext = new Conetxt();
if(nullptr == pContext)
return -1;
return pContext->Init();
}
int Init_Manager()
{
if(nullptr == pManager)
pManager = new CManager();
if(nullptr == pManager)
return -2;
return pManager->Init();
}

最后实现Init函数如下:

int Init()
{
if(Init_Ctx() < 0 )
return -1;
if(Init_Manager() < 0 )
return -2;
return 0;
}

    如果上面的例子中pManager 和pContext对象没有初始化的先后顺序要求,还可以更加简洁一些。

int Init()
{
if(Init_Ctx() < 0 || Init_Manager() < 0)
return -1;
return 0;
}

      从上面的代码中可以看出,经过模块拆分后,代码更加整洁美观,而且功能更加清晰,阅读起来也比较直观。


      写到这里,我想起之前一个项目中处理网络消息的功能,原来的设计是有不同的线程处理不同的业务功能,这些网络消息由RPC模块收发,并注入到这些线程的队列中,这些业务线程只需要读取队列并处理消息,解析得到业务数据即可。

//线程1:处理中心应用服务器消息
void ProcessNetMsg(char* data,unsigned short len)
{
    short id = ParaseprotoBuf(data,len);
    switch(id)
    {
     case ID_POLLING_MSG:
             ReSetPollingTimer();
             break;
         case ID_XXXXX_A:
             ProcessXXXXX_A(data,len);
             break;
          default: break;
    }
}
//线程2:处理区域应用服务器消息
void ProcessNetMsg(char* data,unsigned short len)
{
short id = ParaseprotoBuf(data,len);
switch(id)
{
case ID_POLLING_MSG:
             ReSetPollingTimer();
break;
         case ID_XXXXX_B:
ProcessXXXXX_B(data,len);
break;
default: break;
}
}

     当继续接入新的接口时候,其实每个线程的类中都会充斥着处理消息的类似switch-case的代码,其实不同的人有不同的看法,有些老员工觉得这样挺好的,业务和这个线程关联,在这个类中解析处理是没问题的。


    我还是按照功能去管理,对我们业务来说,其实不需要关心网络包的解析,可能是Json格式,也可能是ProtoBuf序列化的,我们更关系的是拿到业务数据而已。所以,我做了优化重构,参考代码如下所示。

class CCentralServer;
class CLocalServer;
class CNetDataParse
{
    static void ProcessCentralData(CCentralServer* pServer,
              char* data,unsigned short len)
    {
         //解析网络数据,得到业务对象
     class Order = Parse(data,len);
         if(pServer)
         {
              //将业务对象委托给pServer对象进行处理
           pServer->ProcessXXXX(order);
         }
    }
    //....其他....
}

      上面这样的好处是,在CNetDataParse类中只需要解析数据,得到业务数据,然后委托给不同对象中的方法去处理业务数据即可。这样在业务的线程中处理网络消息包和处理业务就解耦合了。


      上面的方法其实还可以在CNetDataParse中初始化的时候注册了不同的业务类,这样还可以进一步优化代码。其实现的思想就是将消息委托给其他类去处理,处理完成再回调不同的业务类中的处理函数去完成处理。


     我们需要知道一个事实,就是本类中的方法也可能在其他类中完成调用。

文章转载自码农的修炼之道,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论