域名查ip地址查询,优化网站公司价格是多少钱,科技与生活,本地网站做淘宝客本文主要起因是#xff0c;一次在微博上和朋友关于嵌套好几层的if-else语句的代码重构的讨论#xff08;微博原文#xff09;#xff0c;在微博上大家有各式各样的问题和想法。按道理来说这些都是编程的基本功#xff0c;似乎不太值得写一篇文章#xff0c;不过我觉得很多…本文主要起因是一次在微博上和朋友关于嵌套好几层的if-else语句的代码重构的讨论微博原文在微博上大家有各式各样的问题和想法。按道理来说这些都是编程的基本功似乎不太值得写一篇文章不过我觉得很多东西可以从一个简单的东西出发到达本质所以我觉得有必要在这里写一篇的文章。不一定全对只希望得到更多的讨论因为有了更深入的讨论才能进步。 文章有点长我在文章最后会给出相关的思考和总结陈词你可以跳到结尾。 所谓箭头型代码基本上来说就是下面这个图片所示的情况。 那么这样“箭头型”的代码有什么问题呢看上去也挺好看的有对称美。但是…… 关于箭头型代码的问题有如下几个 1我的显示器不够宽箭头型代码缩进太狠了需要我来回拉水平滚动条这让我在读代码的时候相当的不舒服。 2除了宽度外还有长度有的代码的if-else里的if-else里的if-else的代码太多读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的。 总而言之“箭头型代码”如果嵌套太多代码太长的话会相当容易让维护代码的人包括自己迷失在代码中因为看到最内层的代码时你已经不知道前面的那一层一层的条件判断是什么样的代码是怎么运行到这里的所以箭头型代码是非常难以维护和Debug的。 微博上的案例 与 Guard Clauses OK我们先来看一下微博上的那个示例代码量如果再大一点嵌套再多一点你很容易会在条件中迷失掉下面这个示例只是那个“大箭头”下的一个小箭头 12345678910111213141516171819202122FOREACH(PtrWfExpression, argument, node-arguments) { int index manager-expressionResolvings.Keys().IndexOf(argument.Obj()); if (index ! -1) { auto type manager-expressionResolvings.Values()[index].type; if (! types.Contains(type.Obj())) { types.Add(type.Obj()); if (auto group type-GetTypeDescriptor()-GetMethodGroupByName(LCastResult, true)) { int count group-GetMethodCount(); for (int i 0; i count; i) { auto method group-GetMethod(i); if (method-IsStatic()) { if (method-GetParameterCount() 1 method-GetParameter(0)-GetType()-GetTypeDescriptor() description::GetTypeDescriptorDescriptableObject() method-GetReturn()-GetTypeDescriptor() ! description::GetTypeDescriptorvoid() ) { symbol-typeInfo CopyTypeInfo(method-GetReturn()); break; } } } } } }} 上面这段代码可以把条件反过来写然后就可以把箭头型的代码解掉了重构的代码如下所示 123456789101112131415161718192021222324FOREACH(PtrWfExpression, argument, node-arguments) { int index manager-expressionResolvings.Keys().IndexOf(argument.Obj()); if (index -1) continue; auto type manager-expressionResolvings.Values()[index].type; if ( types.Contains(type.Obj())) continue; types.Add(type.Obj()); auto group type-GetTypeDescriptor()-GetMethodGroupByName(LCastResult, true); if ( ! group ) continue; int count group-GetMethodCount(); for (int i 0; i count; i) { auto method group-GetMethod(i); if (! method-IsStatic()) continue; if ( method-GetParameterCount() 1 method-GetParameter(0)-GetType()-GetTypeDescriptor() description::GetTypeDescriptorDescriptableObject() method-GetReturn()-GetTypeDescriptor() ! description::GetTypeDescriptorvoid() ) { symbol-typeInfo CopyTypeInfo(method-GetReturn()); break; } }} 这种代码的重构方式叫 Guard Clauses Martin Fowler 的 Refactoring 的网站上有相应的说明《Replace Nested Conditional with Guard Clauses》。Coding Horror 上也有一篇文章讲了这种重构的方式 —— 《Flattening Arrow Code》StackOverflow 上也有相关的问题说了这种方式 —— 《Refactor nested IF statement for clarity》这里的思路其实就是让出错的代码先返回前面把所有的错误判断全判断掉然后就剩下的就是正常的代码了。 抽取成函数 微博上有些人说continue 语句破坏了阅读代码的通畅我觉得他们一定没有好好读这里面的代码其实我们可以看到所有的 if 语句都是在判断是否出错的情况所以在维护代码的时候你可以完全不理会这些 if 语句因为都是出错处理的而剩下的代码都是正常的功能代码反而更容易阅读了。当然一定有不是上面代码里的这种情况那么不用continue 我们还能不能重构呢 当然可以抽成函数 123456789101112131415161718192021222324252627282930313233343536373839bool CopyMethodTypeInfo(auto method, auto group, auto symbol) { if (! method-IsStatic()) { return true; } if ( method-GetParameterCount() 1 method-GetParameter(0)-GetType()-GetTypeDescriptor() description::GetTypeDescriptorDescriptableObject() method-GetReturn()-GetTypeDescriptor() ! description::GetTypeDescriptorvoid() ) { symbol-typeInfo CopyTypeInfo(method-GetReturn()); return false; } return true;}void ExpressionResolvings(auto manager, auto argument, auto symbol) { int index manager-expressionResolvings.Keys().IndexOf(argument.Obj()); if (index -1) return; auto type manager-expressionResolvings.Values()[index].type; if ( types.Contains(type.Obj())) return; types.Add(type.Obj()); auto group type-GetTypeDescriptor()-GetMethodGroupByName(LCastResult, true); if ( ! group ) return; int count group-GetMethodCount(); for (int i 0; i count; i) { auto method group-GetMethod(i); if ( ! CopyMethodTypeInfo(method, group, symbol) ) break; }}......FOREACH(PtrWfExpression, argument, node-arguments) { ExpressionResolvings(manager, arguments, symbol)}...... 你发出现抽成函数后代码比之前变得更容易读和更容易维护了。不是吗 有人说“如果代码不共享就不要抽取成函数”持有这个观点的人太死读书了。函数是代码的封装或是抽象并不一定用来作代码共享使用函数用于屏蔽细节让其它代码耦合于接口而不是细节实现这会让我们的代码更为简单简单的东西都能让人易读也易维护。这才是函数的作用。 嵌套的 if 外的代码 微博上还有人问原来的代码如果在各个 if 语句后还有要执行的代码那么应该如何重构。比如下面这样的代码。 原版123456789101112131415for(....) { do_before_cond1() if (cond1) { do_before_cond2(); if (cond2) { do_before_cond3(); if (cond3) { do_something(); } do_after_cond3(); } do_after_cond2(); } do_after_cond1();} 上面这段代码中的那些 do_after_condX() 是无论条件成功与否都要执行的。所以我们拉平后的代码如下所示 重构第一版123456789101112131415161718192021222324for(....) { do_before_cond1(); if ( !cond1 ) { do_after_cond1(); continue } do_after_cond1(); do_before_cond2(); if ( !cond2 ) { do_after_cond2(); continue; } do_after_cond2(); do_before_cond3(); if ( !cond3 ) { do_after_cond3(); continue; } do_after_cond3(); do_something(); } 你会发现上面的 do_after_condX 出现了两份。如果 if 语句块中的代码改变了某些do_after_condX依赖的状态那么这是最终版本。 但是如果它们之前没有依赖关系的话根据 DRY 原则我们就可以只保留一份那么直接掉到 if 条件前就好了如下所示 重构第二版123456789101112131415for(....) { do_before_cond1(); do_after_cond1(); if ( !cond1 ) continue; do_before_cond2(); do_after_cond2(); if ( !cond2 ) continue; do_before_cond3(); do_after_cond3(); if ( !cond3 ) continue; do_something(); } 此时你会说我靠居然改变了执行的顺序把条件放到 do_after_condX() 后面去了。这会不会有问题啊 其实你再分析一下之前的代码你会发现本来cond1 是判断 do_before_cond1() 是否出错的如果有成功了才会往下执行。而 do_after_cond1() 是无论如何都要执行的。从逻辑上来说do_after_cond1()其实和do_before_cond1()的执行结果无关而 cond1 却和是否去执行 do_before_cond2() 相关了。如果我把断行变成下面这样反而代码逻辑更清楚了。 重构第三版123456789101112131415161718for(....) { do_before_cond1(); do_after_cond1(); if ( !cond1 ) continue; // -- cond1 成了是否做第二个语句块的条件 do_before_cond2(); do_after_cond2(); if ( !cond2 ) continue; // -- cond2 成了是否做第三个语句块的条件 do_before_cond3(); do_after_cond3(); if ( !cond3 ) continue; //-- cond3 成了是否做第四个语句块的条件 do_something(); } 于是乎在未来维护代码的时候维护人一眼看上去就明白代码在什么时候会执行到哪里。 这个时候你会发现把这些语句块抽成函数代码会干净的更多再重构一版 重构第四版123456789101112131415161718192021222324252627282930313233bool do_func3() { do_before_cond2(); do_after_cond2(); return cond3;}bool do_func2() { do_before_cond2(); do_after_cond2(); return cond2;}bool do_func1() { do_before_cond1(); do_after_cond1(); return cond1;}// for-loop 你可以重构成这样for (...) { bool cond do_func1(); if (cond) cond do_func2(); if (cond) cond do_func3(); if (cond) do_something();}// for-loop 也可以重构成这样for (...) { if ( ! do_func1() ) continue; if ( ! do_func2() ) continue; if ( ! do_func3() ) continue; do_something();} 上面我给出了两个版本的for-loop你喜欢哪个我喜欢第二个。这个时候因为for-loop里的代码非常简单就算你不喜欢 continue 这样的代码阅读成本已经很低了。 状态检查嵌套 接下来我们再来看另一个示例。下面的代码的伪造了一个场景——把两个人拉到一个一对一的聊天室中因为要检查双方的状态所以代码可能会写成了“箭头型”。 1234567891011121314151617181920212223242526272829303132int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager){ if ( pA-isConnected() ) { manager-Prepare(pA); if ( pB-isConnected() ) { manager-Prepare(pB); if ( manager-ConnectTogther(pA, pB) ) { pA-Write(connected); pB-Write(connected); return S_OK; }else{ return S_ERROR; } }else { pA-Write(Peer is not Ready, waiting...); return S_RETRY; } }else{ if ( pB-isConnected() ) { manager-Prepare(); pB-Write(Peer is not Ready, waiting...); return S_RETRY; }else{ pA-Close(); pB-Close(); return S_ERROR; } } //Shouldnt be here! return S_ERROR;} 重构上面的代码我们可以先分析一下上面的代码说明了上面的代码就是对 PeerA 和 PeerB 的两个状态 “连上” “未连上” 做组合 “状态” 注实际中的状态应该比这个还要复杂可能还会有“断开”、“错误”……等等状态 于是我们可以把代码写成下面这样合并上面的嵌套条件对于每一种组合都做出判断。这样一来逻辑就会非常的干净和清楚。 123456789101112131415161718192021222324252627282930313233int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager){ if ( pA-isConnected() ) { manager-Prepare(pA); } if ( pB-isConnected() ) { manager-Prepare(pB); } // pA YES pB NO if (pA-isConnected() ! pB-isConnected() ) { pA-Write(Peer is not Ready, waiting); return S_RETRY; // pA NO pB YES }else if ( !pA-isConnected() pB-isConnected() ) { pB-Write(Peer is not Ready, waiting); return S_RETRY; // pA YES pB YES }else if (pA-isConnected() pB-isConnected() ) { if ( ! manager-ConnectTogther(pA, pB) ) { return S_ERROR; } pA-Write(connected); pB-Write(connected); return S_OK; } // pA NO, pB NO pA-Close(); pB-Close(); return S_ERROR;} 延伸思考 对于 if-else 语句来说一般来说就是检查两件事错误 和 状态。 检查错误 对于检查错误来说使用 Guard Clauses 会是一种标准解但我们还需要注意下面几件事 1当然出现错误的时候还会出现需要释放资源的情况。你可以使用 goto fail; 这样的方式但是最优雅的方式应该是C面向对象式的 RAII 方式。 2以错误码返回是一种比较简单的方式这种方式有很一些问题比如如果错误码太多判断出错的代码会非常复杂另外正常的代码和错误的代码会混在一起影响可读性。所以在更为高组的语言中使用 try-catch 异常捕捉的方式会让代码更为易读一些。 检查状态 对于检查状态来说实际中一定有更为复杂的情况比如下面几种情况 1像TCP协议中的两端的状态变化。 2像shell各个命令的命令选项的各种组合。 3像游戏中的状态变化一棵非常复杂的状态树。 4像语法分析那样的状态变化。 对于这些复杂的状态变化其本上来说你需要先定义一个状态机或是一个子状态的组合状态的查询表或是一个状态查询分析树。 写代码时代码的运行中的控制状态或业务状态是会让你的代码流程变得混乱的一个重要原因重构“箭头型”代码的一个很重要的工作就是重新梳理和描述这些状态的变迁关系。 总结 好了下面总结一下把“箭头型”代码重构掉的几个手段如下 1使用 Guard Clauses 。 尽可能的让出错的先返回 这样后面就会得到干净的代码。 2把条件中的语句块抽取成函数。 有人说“如果代码不共享就不要抽取成函数”持有这个观点的人太死读书了。函数是代码的封装或是抽象并不一定用来作代码共享使用函数用于屏蔽细节让其它代码耦合于接口而不是细节实现这会让我们的代码更为简单简单的东西都能让人易读也易维护写出让人易读易维护的代码才是重构代码的初衷 3对于出错处理使用try-catch异常处理和RAII机制。返回码的出错处理有很多问题比如A) 返回码可以被忽略B) 出错处理的代码和正常处理的代码混在一起C) 造成函数接口污染比如像atoi()这种错误码和返回值共用的糟糕的函数。 4对于多个状态的判断和组合如果复杂了可以使用“组合状态表”或是状态机加Observer的状态订阅的设计模式。这样的代码即解了耦也干净简单同样有很强的扩展性。 5 重构“箭头型”代码其实是在帮你重新梳理所有的代码和逻辑这个过程非常值得为之付出。重新整思路去想尽一切办法简化代码的过程本身就可以让人成长。