如何重构“箭头型”代码

本文主要起因是,一次在微博上和朋友关于嵌套好几层的if-else语句的代码重构的讨论(微博原文),在微博上大家有各式各样的问题和想法。按道理来说这些都是编程的基本功,似乎不太值得写一篇文章,不过我觉得很多东西可以从一个简单的东西出发,到达本质,所以,我觉得有必要在这里写一篇的文章。不一定全对,只希望得到更多的讨论,因为有了更深入的讨论才能进步。

文章有点长,我在文章最后会给出相关的思考和总结陈词,你可以跳到结尾。

所谓箭头型代码,基本上来说就是下面这个图片所示的情况。

那么,这样“箭头型”的代码有什么问题呢?看上去也挺好看的,有对称美。但是……

关于箭头型代码的问题有如下几个:

1)我的显示器不够宽,箭头型代码缩进太狠了,需要我来回拉水平滚动条,这让我在读代码的时候,相当的不舒服。

2)除了宽度外还有长度,有的代码的if-else里的if-else里的if-else的代码太多,读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的。

总而言之,“箭头型代码”如果嵌套太多,代码太长的话,会相当容易让维护代码的人(包括自己)迷失在代码中,因为看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,所以,箭头型代码是非常难以维护和Debug的

微博上的案例 与 Guard Clauses

OK,我们先来看一下微博上的那个示例,代码量如果再大一点,嵌套再多一点,你很容易会在条件中迷失掉(下面这个示例只是那个“大箭头”下的一个小箭头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FOREACH(Ptr<WfExpression>, 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(L"CastResult", 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::GetTypeDescriptor<DescriptableObject>() &&
                            method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
                            symbol->typeInfo = CopyTypeInfo(method->GetReturn());
                            break;
                        }
                    }
                }
            }
        }
    }
}

上面这段代码,可以把条件反过来写,然后就可以把箭头型的代码解掉了,重构的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FOREACH(Ptr<WfExpression>, 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(L"CastResult", 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::GetTypeDescriptor<DescriptableObject>() &&
               method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
            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 ,我们还能不能重构呢?

当然可以,抽成函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
bool CopyMethodTypeInfo(auto &method, auto &group, auto &symbol)
{
    if (! method->IsStatic()) {
        return true;
    }
    if ( method->GetParameterCount() == 1 &&
           method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() &&
           method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
        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(L"CastResult", 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(Ptr<WfExpression>, argument, node->arguments) {
    ExpressionResolvings(manager, arguments, symbol)
}
...
...

你发出现,抽成函数后,代码比之前变得更容易读和更容易维护了。不是吗?

有人说:“如果代码不共享,就不要抽取成函数!”,持有这个观点的人太死读书了。函数是代码的封装或是抽象,并不一定用来作代码共享使用,函数用于屏蔽细节,让其它代码耦合于接口而不是细节实现,这会让我们的代码更为简单,简单的东西都能让人易读也易维护。这才是函数的作用。

嵌套的 if 外的代码

微博上还有人问,原来的代码如果在各个 if 语句后还有要执行的代码,那么应该如何重构。比如下面这样的代码。

原版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(....) {
    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() 是无论条件成功与否都要执行的。所以,我们拉平后的代码如下所示:

重构第一版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for(....) {
    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 条件前就好了,如下所示:

重构第二版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(....) {
    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() 相关了。如果我把断行变成下面这样,反而代码逻辑更清楚了。

重构第三版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for(....) {
    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();
  
}

于是乎,在未来维护代码的时候,维护人一眼看上去就明白,代码在什么时候会执行到哪里。 这个时候,你会发现,把这些语句块抽成函数,代码会干净的更多,再重构一版:

重构第四版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool 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 ,这样的代码阅读成本已经很低了。

状态检查嵌套

接下来,我们再来看另一个示例。下面的代码的伪造了一个场景——把两个人拉到一个一对一的聊天室中,因为要检查双方的状态,所以,代码可能会写成了“箭头型”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int 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;
        }
    }
    //Shouldn't be here!
    return S_ERROR;
}

重构上面的代码,我们可以先分析一下上面的代码,说明了,上面的代码就是对 PeerA 和 PeerB 的两个状态 “连上”, “未连上” 做组合 “状态” (注:实际中的状态应该比这个还要复杂,可能还会有“断开”、“错误”……等等状态), 于是,我们可以把代码写成下面这样,合并上面的嵌套条件,对于每一种组合都做出判断。这样一来,逻辑就会非常的干净和清楚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int 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) 重构“箭头型”代码其实是在帮你重新梳理所有的代码和逻辑,这个过程非常值得为之付出。重新整思路去想尽一切办法简化代码的过程本身就可以让人成长。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/452518.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Django博客--4.开发博客文章详情页

文章目录0.思路引导1.设计文章详情页的 URL2.获取文章的URL3.编写 detail 视图函数4.编写详情页模板5.更改主页中跳转详情页的地址链接6.模板继承--抽取base.html7.模板继承--修改 index.html使其继承base.html8.模板继承--修改detail.html使其继承base.html9.结果展示0.思路引…

10、并发容器,ConcurrentHashMap

Java 提供了不同层面的线程安全支持。在传统集合框架内部&#xff0c;除了 Hashtable 等同步容器&#xff0c;还提供了所谓的同步包装器&#xff08;Synchronized Wrapper&#xff09;&#xff0c;我们可以调用 Collections 工具类提供的包装方法&#xff0c;来获取一个同步的包…

程序员的本质

Computers are useless. They can only give you answers. – Picasso计算机没有什么作用。他们只能告诉你答案。——毕加索很多人&#xff08;包括我岳母&#xff09;认为计算机变得如此智能&#xff0c;所以在不久的未来将不再需要程序员。另外一些人认为程序员是天才&#x…

剖析管理所有大数据组件的可视化利器:Hue

欢迎关注大数据和人工智能技术文章发布的微信公众号&#xff1a;清研学堂&#xff0c;在这里你可以学到夜白&#xff08;作者笔名&#xff09;精心整理的笔记&#xff0c;让我们每天进步一点点&#xff0c;让优秀成为一种习惯&#xff01; 日常的大数据使用都是在服务器命令行中…

Django博客--5.让博客支持 Markdown 语法和代码高亮

文章目录0.前言1.安装 Python Markdown2.在 detail 视图中解析 Markdown3.safe 标签4.代码高亮5.效果展示0.前言 Markdown 是一种 HTML 文本标记语言&#xff0c;只要遵循它约定的语法格式&#xff0c;Markdown 的解析工具就能够把 Markdown 文档转换为标准的 HTML 文档&#…

Diango博客--6.Markdown 文章自动生成目录

文章目录0.思路引导1.在文中插入目录2.在页面的任何地方插入目录3.美化标题的锚点 URL0.思路引导 Markdown 在解析内容的同时还可以自动提取整个内容的目录结构&#xff0c;本文内容将从以下几个方面展开&#xff1a; 1&#xff09;在文中插入目录&#xff1b; 2&#xff09;在…

Java中对象和引用的理解

2019独角兽企业重金招聘Python工程师标准>>> 偶然想起Java中对象和引用的基本概念&#xff0c;为了加深下对此的理解和认识&#xff0c;特地整理一下相关的知识点&#xff0c;通过具体实例从两者的概念和区别两方面去更形象的认识理解&#xff0c;再去记忆。12一、对…

android怎样封装,如何封装属于自己的博客网站安卓APP 源码家园

说实话我今天在写这个文章的时候是我使用易语言(E4A\易安卓)的第一天&#xff0c;我也是易小白&#xff0c;但是的确可以用&#xff01;我为什么写这个文章呢&#xff1f;因为之前我也想封装自己的网站&#xff0c;然后去网上找的在线封装生成APP&#xff0c;果然能封装好了&am…

Diango博客--7.自动生成文章摘要

文章目录0.思路引导1.方法一&#xff1a;覆写 save 方法2.方法二&#xff1a;使用 truncatechars 模板过滤器0.思路引导 博客文章的模型有一个 excerpt 字段&#xff0c;这个字段用于存储文章的摘要。 若在 django admin 后台手动为文章输入摘要&#xff0c;每次手动输入摘要…

特斯拉股价暴跌,疯狂烧钱是否真的能够带来高额回报?

“疯狂烧钱”并不能成为公司持续亏损的理由&#xff0c;反而可能成为公司升级转型的关键所在。 上周三&#xff0c;特斯拉发布第四季度财报&#xff0c;其后特斯拉CEO马斯克在电话会议上表示&#xff0c;特斯拉亏损收窄&#xff0c;营收同比增长88%&#xff0c;但与此同时其首…

Diango博客--8.解锁博客侧栏

文章目录0.思路引导1.[最新文章] 模板标签2.[归档] 模板标签3.[分类] 模板标签4.[标签云] 模板标签5.使用自定义的模板标签0.思路引导 博客侧边栏有四项内容&#xff1a;最新文章、归档、分类和标签云&#xff0c;效果展示如下&#xff1a; 这些内容相对比较固定和独立&…

十五、详述 IntelliJ IDEA 插件的安装及使用方法

正文 首先&#xff0c;进入插件安装界面&#xff1a; Mac&#xff1a;IntelliJ IDEA -> Preferences -> Plugins;Windows&#xff1a;File -> Settings -> Plugins.标注 1&#xff1a;显示 IntelliJ IDEA 的插件分类&#xff0c; All plugins&#xff1a;显示 Inte…

面向数据流的设计方法

面向数据流的设计方法的目标是给出设计软件结构的一个系统化的途径。 在软件工程的需求分析阶段&#xff0c;信息流是一个关键考虑。通常用数据流图描绘信息在系统中加工和流动的 情况。面向数据流的设计方法定义了一些不同的“映射”&#xff0c;利用这些映射可以把数据流图…

AI研究的盲点:无解的神经网络内在逻辑

论人工神经网络内在逻辑的研究历史及现状。 伴随着大数据&#xff0c;人工智能&#xff08;AI&#xff09;在沉寂了多年之后&#xff0c;又迎来了新的高潮。在这场涉及大部分科学的革命中&#xff0c;人工神经网络释放了人工智能&#xff08;AI&#xff09;。但科学家们发现&a…

Diango博客--9.归档、分类和标签页

文章目录0.思路引导1.回顾2.归档页面3.分类页面4.标签页面0.思路引导 侧边栏已经正确地显示了最新文章列表、归档、分类、标签等信息&#xff0c;现在来完善归档、分类和标签功能。 当用户点击归档下的某个日期、分类栏目下的某个分类或者标签栏目下的某个标签时&#xff0c;…

android studio1.2.6,1.2.2 使用Android Studio开发Android APP | 菜鸟教程

写在前面本节将介绍如何使用Android Studio开发Android APP&#xff0c;和前面Eclipse ADT SDK搭建Android开发环境一样&#xff0c;本节也只是介绍一些基本东西&#xff0c;深入的&#xff0c;比如快捷键&#xff0c;小技巧等会再另一篇文章中详细地介绍&#xff01;1.下载A…

UPS开始尝试“货车+无人机”的投递方式,不必再担心快递员离职了

继亚马逊“空中仓库”&#xff0c;无人机送货再现新形式。 作为世界上最大的快递承运商与包裹递送公司&#xff0c;UPS当然也没有放过“送货无人机”这一新颖业务。与亚马逊推出“空中仓库”的理念类似&#xff0c;UPS并没有选择让无人机从仓库直接起飞&#xff0c;而是将之与…

Diango博客--10.交流的桥梁“评论功能”

文章目录0.思路引导1.创建"评论"应用2.设计"评论"的数据库模型3.注册"评论"模型到 admin4.设计“评论”表单5.展示评论表单6.“评论”视图函数7.绑定 URL8.向读者发送是否“评论”成功的状态9.详情页底部显示“评论”内容0.思路引导 本文将创建…

python与android交互,Android客户端与Python服务器端的简单通信

最近在做一个APP&#xff0c;需要与服务器通信&#xff0c;一点一点的尝试&#xff0c;记录一下。本文使用了OkHttp和Flask框架。Android客户端&#xff1a;实现功能输入完点击OK按钮后会toast成功的信息。Python服务端&#xff1a;各部分代码如下&#xff1a;activity_main.xm…

云栖科技评论第48期:前沿科技对世界的改造 我们这代人只完成了1%

1、数字经济版图呈中美双分趋势 日本IT行业为前景担忧 数字经济版图呈中美双分趋势 日本IT行业为前景担忧 【新闻摘要】《日本经济新闻》日前刊文称&#xff0c;数字经济的势力版图呈现中国和美国两强双分的趋势明显&#xff0c;这意味着日本可能不得不使用中美的技术&#xff…