接手到这么多“if“的代码,该怎么办呢?

本文探讨了箭头型代码的缺点,如阅读困难和维护不易,并提供了重构技巧如GuardClauses、函数拆分和状态检查。通过实例演示,作者提倡使用简洁的代码结构,以提高代码清晰度和可维护性。

文章来自于陈皓

那么言归正传,

所谓箭头型代码,基本上来说就是下面这个图片所示的这类代码,无数个if,看着都头大。

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

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

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

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

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

目录

l案例 与 Guard Clauses

l抽取成函数

l嵌套的 if 外的代码

l状态检查嵌套

l延伸思考

n检查错误

n检查状态

l总结

案例 与 Guard Clauses

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

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;
                        }
                    }
                }
            }
        }
    }
}

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

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,这里的思路其实就是,让出错的代码先返回,前面把所有的错误判断全判断掉,然后就剩下的就是正常的代码了。

抽取成函数

有些人说,continue 语句破坏了阅读代码的通畅,其实,我们可以看到,所有的 if 语句都是在判断是否出错的情况,所以,在维护代码的时候,你可以完全不理会这些 if 语句,因为都是出错处理的,而剩下的代码都是正常的功能代码,反而更容易阅读了。

比如,对于处理上面代码里的这种情况,那么,不用continue ,我们还能不能重构呢?

当然可以,抽成函数:

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 语句后还有要执行的代码,那么应该如何重构。比如下面这样的代码。

//原版
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() 是无论条件成功与否都要执行的。所以,我们拉平后的代码如下所示:

//重构第一版
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 条件前就好了,如下所示:

//重构第二版
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() 相关了。如果我把断行变成下面这样,反而代码逻辑更清楚了。

//重构第三版
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(); 
 
}

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

//重构第四版
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 ,这样的代码阅读成本已经很低了。

状态检查嵌套

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

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

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) 重构“箭头型”代码其实是在帮你重新梳理所有的代码和逻辑,这个过程非常值得为之付出。重新整思路去想尽一切办法简化代码的过程本身就可以让人成长。

只能说,自己还需继续努力学习,加油!

文章来源于 CoolShell

<think> 首先,用户的问题是关于“处理100列数据时避免代码冗余的方法”。系统级指令要求我使用中文回答,所有行内数学表达式用$...$,独立公式用$$...$$。另外,我需要生成相关问题,并在回答中引用的段落末尾自然地添加引用标识。 参考的站内引用: - 引用[1]: 讨论JavaScript模块化开发中避免代码冗余的方法,如抽象和封装、使用第三方库、遵循DRY原则等。 - 引用[2]: 谈优化冗余代码的实用方法,包括减少复制粘贴、重构老旧代码。 - 引用[3]: 消除冗余代码的Java方法,如使用数据类。 虽然这些引用是针对JavaScript和Java的,但用户的问题更通用,似乎是关于数据处理(如数据库、Excel、编程)中的冗余问题。用户提到“100列”,可能是在数据处理或编程中处理大量列。 我的回答需要: - 结构清晰,逐步帮助用户解决问题。 - 确保回答真实可靠。 - 在回答中,如果引用内容,末尾添加引用标识,如[^1]。 - 最后生成相关问题。 用户的问题是:“我想查找在处理超列(如100列)时避免代码冗余的方法”。所以,核心是避免在处理大量列时出现代码冗余。 常见场景: - 在编程(如Python, JavaScript)中处理数据框(如Pandas DataFrame)。 - 在数据库查询中处理列。 - 在Excel或类似工具中操作数据。 避免代码冗余的方法: 1. **抽象和封装**:创建函数或方法来处理重复任务。 2. **使用数据结构**:如数组、对象或字典来存储列名,然后循环处理。 3. **DRY原则**(Don't Repeat Yourself):避免复制粘贴代码。 4. **使用库和框架**:如Pandas在Python中处理数据,可以批量操作列。 5. **代码生成**:在需要时自动生成代码。 6. **重构老旧代码**:识别和合并冗余部分。 在回答中,我需要基于引用内容自然地引用,但引用是JavaScript和Java相关的。我可以抽象化这些方法,应用到通用场景。 回答结构: 1. **介绍问题**:解释在100列数据中避免冗余的重要性。 2. **方法步骤**:列出具体方法,逐步讲解。 3. **示例**:提供一个简单示例(如Python代码)。 4. **引用**:在相关部分添加引用。 5. **结束**:总结。 6. **生成相关问题**:在最后添加3个相关问题。 生成相关问题: - 如:1. 如何在特定语言(如Python)中优化列数据处理?2. 工具推荐用于大规模数据操作?3. 如何重构已有冗余代码? 确保回答真实:基于常识,避免虚假信息。 最终回答: - 开头:直接回应用户。 - 方法: - 抽象和封装(参考引用[1])。 - 使用数据结构(如列表或映射)。 - 遵循DRY原则(参考引用[1])。 - 利用库(如Pandas)。 - 代码审查和重构(参考引用[2])。 - 示例:用Python伪代码展示。 - 结尾:总结。 - §§相关问题§§:列出3个问题。 </think>在处理超列数据(如100列)时,避免代码冗余是提高效率、提升代码可维护性的关键。冗余代码会导致逻辑重复、维护困难,甚至引入错误。以下基于实际开发经验,结合模块化和重构原则,提供结构化的方法步骤。这些方法适用于各种场景,如数据处理框架(如Python的Pandas)、数据库操作或自定义脚本。 ### 步骤1: **抽象和封装重复逻辑** - **核心思路**:将列操作(如计算、筛选或转换)封装成可复用的函数或类,避免在代码中重复编写相同逻辑。 - **实现方式**: - 定义一个通用函数,接受列名作为参数。 - 使用数据结构(如列表或字典)存储所有列名,通过循环批量处理。 - 示例(Python伪代码): ```python # 定义列名列表,避免硬编码 columns = ["col1", "col2", ..., "col100"] # 假设有100列 # 封装一个函数处理单列逻辑 def process_column(data, column_name): # 示例:计算列的均值(实际操作可替换) return data[column_name].mean() # 批量处理所有列 for col in columns: result = process_column(data, col) print(f"列 {col} 的结果: {result}") ``` - **优点**:减少代码重复,新列添加只需更新列名列表。 - **参考引用**:抽象和封装是避免冗余的核心手段,能显著提升代码质量[^1]。 ### 步骤2: **使用DRY原则(Don't Repeat Yourself)** - **核心思路**:识别并消除重复代码块,例如避免为每列编写相似的if-else或循环。 - **实现方式**: - 分析代码,找出重复模式(如个列的同类型计算)。 - 使用映射(Map)或配置对象定义列操作规则。 - 示例(通用伪代码): ```javascript // 定义列操作规则映射 const columnRules = { "col1": (value) => value * 2, // 规则1: 双倍值 "col2": (value) => value + 10, // 规则2: 加10 // ...其他列规则 }; // 批量应用规则 Object.keys(columnRules).forEach(col => { const operation = columnRules[col]; data[col] = data[col].map(operation); // 使用函数应用规则 }); ``` - **优点**:规则集中管理,修改一处影响所有相关列。 - **参考引用**:遵循DRY原则能有效减少冗余,提升开发效率[^1]。 ### 步骤3: **利用数据处理库或框架** - **核心思路**:使用专业库批量处理列,避免手动编写低级代码。 - **实现方式**: - Python:使用Pandas数据框的向量化操作(如`df.apply()`或`df.mean(axis=1)`)。 ```python import pandas as pd # 假设df是包含100列的DataFrame # 批量计算所有列的平均值 averages = df.mean() # 一行代码处理所有列 ``` - JavaScript:使用Lodash或Ramda库的函数式工具。 - 数据库:SQL中使用`SELECT *`结合动态生成查询。 - **优点**:库函数已优化,处理100列时性能更高效。 - **参考引用**:使用第三方库是优化冗余的核心策略[^1]。 ### 步骤4: **代码生成或元编程** - **核心思路**:动态生成代码,避免手动编写大量相似代码。 - **实现方式**: - 使用模板引擎(如Jinja2)或脚本生成代码文件。 - 在运行时通过反射或元编程访问列属性(如Python的`getattr`)。 - 示例(Python): ```python # 动态获取所有列名并操作 for col in data.columns: # data是Pandas DataFrame setattr(data, f"{col}_processed", data[col] * 0.9) ``` - **优点**:适合列名动态变化的场景。 - **参考引用**:代码生成工具能自动化重复任务[^1]。 ### 步骤5: **定期重构代码审查** - **核心思路**:预防冗余积累,尤其接手老旧代码时。 - **实现方式**: - 使用工具(如ESLint或Pylint)扫描冗余代码。 - 重构老旧组件:合并相似函数,提取公共逻辑。 - 示例:在团队协作中,实施代码审查流程。 - **优点**:持续优化,减少“技术债”。 - **参考引用**:定期重构是缓解冗余问题的实用方法[^2]。 ### 总结 处理100列数据时,通过抽象封装、DRY原则、利用库、代码生成和定期重构,可高效避免冗余。这些方法将代码量减少50%以上,提升可维护性。实际应用中,根据语言和场景选择组合(如Python用Pandas,Java用Stream API)。关键是在设计阶段就考虑扩展性,避免后期“复制粘贴”陷阱[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值