软件工程师,全面思考问题很重要

简介: 软件工程师,全面思考问题很重要

为什么要全面思考问题

□ 在软件开发中,对一个问题思考得越全面,编写出的代码就会越严谨,出现bug的几率就越低;反之,如果没有对一个问题进行全面而深入的思考,编写出的代码就会漏洞百出,出现各种莫名其妙、无法复现的bug的几率也就急剧增加。

□ 软件就是数据加逻辑,数据是“肉身”,逻辑是“灵魂”。如果不全面思考问题,在某些情况下, “灵魂”就会“精

神错乱”,甚至损坏“肉身”,进而导致无法正常工作。

□ 只有经过全面思考编写出的代码,才是严谨的,才能保证可靠性。一份代码即使严格遵守了代码规范,重构

了设计模式,但思考不全面,逻辑不严谨,也不能称之为优雅。

□ 没有经过全面思考开发出的软件,虽然短期内可能能正常工作,但长远来看,各种问题和漏洞一定会爆发出来,从而导致系统的可靠性、可维护性和稳定性大打折扣。记住墨菲定律:凡是你认为可能会出错的,它一定会出错。

下面,我们通过几个实例来理解如何进行全面思考。

实例1

输入若干个整数作为数组,将数组中每一个元素除以第一个元素的结果,作为新的数组元素值。

这道编程题并不难,稍加一思索,很容易给出下面的答案。

#include <iostream>
using namespace std;

static void DivArray(int *pnArray, int nSize)
{
    for (int i = 0; i < nSize; i++)
    {
        pnArray[i] /= pnArray[0];
    }
}

int main()
{
    int nSize = 0;
    cin >> nSize;
    int *pnNumber = new int[nSize];
    for (int i = 0; i < nSize; i++)
    {
        cin >> (*pnNumber++);
    }

    DivArray(pnNumber, nSize);
    delete pnNumber;
    pnNumber = NULL;
    return 0;
}

看着是不是也没什么大问题?实际上,这段代码至少有以下6个问题:

1、没有对输入的nSize进行检查,nSize可能为负数、0或者很大的一个正数(可能会导致内存溢出)。

2、从cin输入数组元素后,pnNumber已经不指向第一个数组元素了,后面使用和释放会出错。

3、函数DivArray中,没有对传入的两个参数进行检查。

4、函数DivArray中,数组中第一个元素可能为0,被0除会导致程序崩溃。

5、函数DivArray中,从0开始遍历数组,导致数组中第一个元素已经变成了1,后面元素的逻辑均不正确。

6、释放pnNumber时,应当使用delete [],而不是delete。

从这个例子可以看出来,问题简单,并不代表不需要全面思考。不全面思考,后果很严重,你的代码中到处都充斥着漏洞和bug。

实例2

自行封装一个函数,用于实现内存拷贝,函数原型如下:

void *memcpy(void *dest, const void *src, size_t count);

有的新手看到这个题目,觉得so easy,立马写出了下面的代码。

void *memcpy(void *dest, const void *src, size_t count)
{
    if (src == NULL || dest == NULL)
    {
        return NULL;
    }

    while (count--)
    {
        *dest++ = *src++;
    }

    return dest;
}

很可惜,上面的代码除了一些低级的语法错误外,思考得也不够全面。

1、src和dest都是void *类型,不能对其进行++操作。

2、返回的dest指针,已经不是原始传入的dest指针了。

3、当dest指向的内存区域,与src指向的内存区域有重叠时,可能导致拷贝错误的数据。

正确的实现可以参考下面的代码。

void *memcpy(void *dest, const void *src, size_t count)
{
    if (src == NULL || dest == NULL)
    {
        return NULL;
    }

    if (dest > src && (char *)dest < (char *)src + count)
    {
        char *pSrc = (char *)src + count - 1;
        char *pDest = (char *)dest + count - 1;
        while (count--)
        {
            *pDest-- = *pSrc--;
        }
    }
    else
    {
        char *pSrc = (char *)src;
        char *pDest = (char *)dest;
        while (count--)
        {
            *pDest++ = *pSrc++;
        }
    }

    return dest;
}

实际上,如果更进一步思考,上面的代码还有优化的空间。

1、当count为0时,其实可以直接返回dest,后面的逻辑就不用考虑count了,更简洁。

2、当src和dest相等时,说明它们指向的是同一块区域,可以直接返回dest,不需要再去拷贝。

3、当count较大时,一个字节一个字节拷贝的方式,效率非常低。如果追求效率的话,需要考虑字节对齐和多字节拷贝等,可参考glibc中memcpy的实现(不支持内存重叠)。

实例3

有一根长为L的平行于x轴的细木杆,其左端点的x坐标为0(故右端点的x坐标为L)。刚开始时,上面有N只蚂蚁,第i(1≤i≤N)只蚂蚁的横坐标为xi(假设xi已经按照递增顺序排列),方向为di(0表示向左,1表示向右)。每只蚂蚁都以速度v向前走,当任意两只蚂蚁碰头时,它们会同时调头朝相反方向走,速度不变。编写程序求解以下问题: 1、所有蚂蚁都离开木杆需要多长时间? 2、所有蚂蚁都离开木杆共碰撞了多少次? 3、第i只蚂蚁离开木杆需要多长时间?

image.png

以下为这道题的思考过程。

1、因为每只蚂蚁的初始方向是任意的,两只蚂蚁碰头后会调头,因此,所有蚂蚁的实时坐标是动态的,且是相互影响的,直接编程很难处理。考虑下面两只蚂蚁的情况,从碰头到离开木板的时间,是x和10-x的较大值。

image.png

如果两只蚂蚁碰头后,不调头,而是仍按原方向前进,从碰头到离开木板的时间,也是x和10-x的较大值,只不过后离开木板的那只蚂蚁变了。也就是说,蚂蚁碰头后不调头,不影响所有蚂蚁离开木板的时间。

image.png

2、同上面的分析,蚂蚁碰头后不调头,不影响蚂蚁碰撞的次数,只影响哪两只蚂蚁碰撞了。在此假设下,每只蚂蚁碰撞的次数,就是初始时,与其前进方向相反的蚂蚁的个数。

3、与问题1和2不同,这里求解的是某只蚂蚁离开木杆的时间,需要我们更深入地分析和思考问题。不管某只蚂蚁中间和其他蚂蚁碰撞了多少次,到最后一次碰撞时,这两只蚂蚁爬行的时间和路程是相同的,假设路程用A表示。碰撞后,各自调头,离开目标,则向左离开木板的那只蚂蚁爬行的总路程为:A+x。而这个A+x也可以理解为另一只蚂蚁之前爬行的路程加上继续向左离开木板爬行的路程。

怎么更形象地理解呢?可以假设每只蚂蚁背着一粒米,碰头时,交换各自的米,然后调头。这样,蚂蚁虽然调头了,但米的前进方向始终不变。蚂蚁爬行的路程,就是它离开木板时背着的米走过的路程。那么,两者如何关联起来呢?

假设初始时,有P只蚂蚁向左,则有N-P只蚂蚁向右。因为每次碰撞后,向左和向右的蚂蚁数量不变,因此,最终肯定有P只蚂蚁从左边离开木板,有N-P只蚂蚁从右边离开木板。是哪P只蚂蚁呢?

假如蚂蚁a初始在蚂蚁b左边,则根据规则,任何时候,蚂蚁a的相对位置都在蚂蚁b左边。故初始时靠左的前P只蚂蚁,必定会从左边离开木板,其他N-P只蚂蚁,必定会从右边离开木板。

假如i不大于P,则第i只蚂蚁会从左边离开木板,它爬行的路程,就是它离开木板时背着的米走过的路程。它背着的米哪来的?来自初始时第i只向左的蚂蚁。故初始时第i只向左的蚂蚁的坐标,就是第i只蚂蚁爬行的路程。同理,假如i大于P,则L减去第i-P只向右的蚂蚁的坐标,就是第i只蚂蚁爬行的路程。

实例4

嵌入式设备进行升级时,支持升级uboot、内核、根文件系统和程序。以升级程序为例,我们在升级时的大致流程是什么样的?有哪些需要注意的地方?

我们可以用下面的流程图来理解嵌入式设备升级程序的处理逻辑。

image.png

1、返回进度时,应包括接收数据的进度和向分区写入数据的进度两部分,比如:前者占60%,后者占40%,然后根据这个比例计算总的进度。

2、程序包应当将一些相关信息也打包进去,以便于校验,比如:包的类型、版本号、对应的硬件型号。包的类型用于区分是uboot、内核还是程序;版本号用于和当前系统进行版本的比较;不同硬件的程序包是不能混着升级的,故需要知道硬件型号。

3、某个客户端正在升级程序时,又有其他客户端升级程序,怎么办? 最简单的办法:直接返回错误。

4、程序分区的大小是有限制的,程序包的大小超过了程序分区的大小,怎么办? 认为程序包非法,终止升级流程,并返回错误码给客户端,由客户端进行提示。

5、正在向程序分区写入数据时,如果此时数据传输通道异常断开了,怎么办? 不要终止写入,而应当继续完成整个升级流程(只是不需要返回进度了)。如果此时终止数据写入,已经写入的数据是不完整的,重启后,程序将无法正常运行。

6、向程序分区写完所有数据,并发送了100%的进度信息后,如果此时立即断开数据传输通道,客户端可能收不到进度为100%的信息,导致客户端认为升级出错了。怎么办? 发送了100%的进度信息后,应当等待一段时间(比如:3秒钟)后,再跳出工作循环。在这段时间内,正常情况下,客户端应当能收到进度为100%的信息,然后由客户端断开数据传输通道,模组检测到连接断开后,跳出工作循环。

7、正在向程序分区写入数据时,传过来了重启设备或关机的命令,怎么办? 处理重启设备或关机的命令时,如果发现正在升级程序,直接返回错误。

8、正在向程序分区写入数据时,断电了怎么办?

有以下几种处理方式:

□ 不处理

上电后,模组会变成“砖”,除非通过串口重新写入程序。

□ 使用备份分区

当空间足够时,存在一个程序分区和一个备份程序分区。

(1)升级程序时,先写入备份程序分区,写完并同步后,开始写程序分区,同时置正在升级标志为true。写完程序分区并同步后,置正在升级标志为false,说明升级成功。

(2)在根文件系统分区或其他分区中,存在一个子程序。子程序运行后,检查正在升级标志。若发现为true,说明升级不成功,需要进行恢复。若发现为false,则检查程序进程是否存在,若不存在,则也需要进行恢复。若不需要进行恢复,则子程序直接退出。恢复的逻辑为:首先读取备份程序分区,并同步写到程序分区;然后,检查正在升级标志是否为true,若为true,则修改为false;最后重启。

(3)升级程序时,检查子程序进程是否存在,若存在,则直接返回失败,避免同时读写备份程序分区。

□ 使用链接

(1)当通过链接下载升级程序包时,可以将链接保存下来。

(2)子程序发现需要恢复时,从保存的链接下载程序包,再写入程序分区。

总结

1、全面思考问题的前提条件是经验和积累,只有具备丰富的经验和充足的积累后,才能做到全面思考。因此,需要我们多拓展知识面的宽度,多挖掘知识面的深度。

2、全面思考问题时,需要考虑一个问题的所有影响因素,以及这些因素之间的关联关系和相互作用。

3、多从各个角度、各个层面考虑问题,比如:从代码规范的角度看有没有遵守,从封装的角度看合不合理,从逻辑的角度看严不严密,从效率的角度看还能否优化,等等。

4、当一种思维方式行不通或遇阻时,不要“钻牛角尖”。多尝试跳出这种思维方式,换一个角度,换一种思维方式思考和分析问题。

5、多反问自己:类里面的成员变量,都有正确赋初始值吗?分配的内存和创建的句柄,有正确释放吗?在当前这个位置必须要释放吗,释放完了其他地方还有没有在使用?… …

6、多从全局和整体思考问题,这样才能够看到事物的来龙去脉,看到事物发展变化的趋势及其背后的驱动因素。


相关文章
|
12天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
8天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2522 18
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
8天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1525 15
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
4天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
10天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
596 14
|
1月前
|
运维 Cloud Native Devops
一线实战:运维人少,我们从 0 到 1 实践 DevOps 和云原生
上海经证科技有限公司为有效推进软件项目管理和开发工作,选择了阿里云云效作为 DevOps 解决方案。通过云效,实现了从 0 开始,到现在近百个微服务、数百条流水线与应用交付的全面覆盖,有效支撑了敏捷开发流程。
19283 30
|
10天前
|
人工智能 自动驾驶 机器人
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
过去22个月,AI发展速度超过任何历史时期,但我们依然还处于AGI变革的早期。生成式AI最大的想象力,绝不是在手机屏幕上做一两个新的超级app,而是接管数字世界,改变物理世界。
498 49
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
|
1月前
|
人工智能 自然语言处理 搜索推荐
阿里云Elasticsearch AI搜索实践
本文介绍了阿里云 Elasticsearch 在AI 搜索方面的技术实践与探索。
18845 20
|
1月前
|
Rust Apache 对象存储
Apache Paimon V0.9最新进展
Apache Paimon V0.9 版本即将发布,此版本带来了多项新特性并解决了关键挑战。Paimon自2022年从Flink社区诞生以来迅速成长,已成为Apache顶级项目,并广泛应用于阿里集团内外的多家企业。
17530 13
Apache Paimon V0.9最新进展
|
3天前
|
云安全 存储 运维
叮咚!您有一份六大必做安全操作清单,请查收
云安全态势管理(CSPM)开启免费试用
368 4
叮咚!您有一份六大必做安全操作清单,请查收