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

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

为什么要全面思考问题

□ 在软件开发中,对一个问题思考得越全面,编写出的代码就会越严谨,出现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、多从全局和整体思考问题,这样才能够看到事物的来龙去脉,看到事物发展变化的趋势及其背后的驱动因素。


相关文章
|
3月前
|
机器学习/深度学习 人工智能 开发者
技术之道:从迷茫到明晰的自我探索
在技术的海洋里,每位开发者都是在不断试错和成长的旅程中。本文通过个人经历,探讨了如何从初入职场的迷茫中找到自己的技术方向,并分享了持续学习和实践的重要性。
48 4
|
文字识别 算法 NoSQL
读书分享:《程序员修炼之道:通向务实的最高境界》的思想经验
相较于全书众多的干货笔记,这篇文章是个别思想经验的总结,希望和大家交流。 ETC;DRY不仅限于编码;维护一个项目概念列表;帮助业务方理解他想要什么;防御性编程;继承税;学会沟通;小实验
读书分享:《程序员修炼之道:通向务实的最高境界》的思想经验
|
设计模式 算法 程序员
代码能力,程序员自我修养之基石
提高代码能力不是一蹴而就的事,需要我们不断努力,通过持续学习和练习、参与开源项目、阅读优秀的代码、与他人合作、提升解决问题的能力等方式,提高自己的代码能力,为自己为公司创造价值。
241 0
代码能力,程序员自我修养之基石
|
架构师
架构师的自我修养
软件架构,指从宏观角度说明一套软件系统的组成和特性。 架构设计与需求分析,概要设计,详细设计最大的区别在于“宏观”二字。要去架构师必须具有大局观,从全局角度思考问题。
191 0
架构师的自我修养
|
敏捷开发 架构师 测试技术
软技能2:软件开发者职业生涯指南-读书笔记
整书有很多内容,从成为一名软件开发者一直到完整的职业生涯,这里只是记录自己阅读过程中感受最深或者最受用的部分。
|
Java 程序员 测试技术
《代码整洁之道》&《程序员的职业素养》
《代码整洁之道》&《程序员的职业素养》
593 0
软件工程师的职业规划
电信、银行等行业一直是许多人非常向往的工作单位,清差厚禄,旱涝保收,陈皓却不以此为然。所以当记者采访他的时候,他连用了两个“最”字来形容他离开银行的成就感。 陈皓毕业后的前两年就职于云南省工商银行,从事银行电信内全国性业务系统开发。
1541 0
|
程序员
程序员修炼的务实哲学
虽然软件开发不受绝大多数物理法则的约束,但我们无法躲避来自熵的增加的重击。熵是一个物理学术语,它定义了一个系统的“无序”总量。不幸的是,热力学法则决定了宇宙中的熵会趋向最大化。当软件中的无序化增加时,程序员会说“软件在腐烂”。