从脏乱差工程得到的一些启示

(点击上方公众号,可快速关注)

从事计算机软件行业,或多或少都经历过一些难以言表的脏乱差工程,这篇文章说的是这些脏乱差工程的直接表现,以及简单的原因分析和解决方案。由于这是一个非常大的话题,本篇文章侧重于我经历过的项目,力求篇幅不要太大。

背景

脏乱差”是一个主观色彩比较浓重的词,每个人的标准都不太一样。下文是以我的视角所展开的一些阐述,但应该大部分是共识,谁让我们都是喜爱穿格子衫的人呢。

我想每个人都接手过前人留下的遗产工程,如果你运气好的话,这些工程可读性好、扩展性好,你能快速地上手并加以迭代,最后能顺利得过渡到自己的框架体系下。但从我有限的经历看,这是颇具运气的事,因为我所接手的工程基本上清一色的脏乱差的代码,维护成本超高。这样的代码所造成的最大的影响是:时间成本巨大,一个简单的三五分钟的修改,需要反反复复的验证,这样的验证不止于代码阅读,大多数情况下还要写各种不同的测试用例验证不同情况下的输出。这套流程下来,有时候甚至几天,这真不是危言耸听。这既给公司造成了浪费,也让个人的成长缺乏成就感和明显的绩效。

如果你遇到这种情况,能怎么处理呢,要么放弃这个烂摊子(这往往不是你能决定的),要么开始长达数月的重构之旅。你不重构,这个问题会反反复复地骚扰你。本文不阐述重构的技巧,具体重构的技术、技巧可以阅读《重构:改善既有代码的设计》这本书。

再多说一句,为什么我们要重构代码呢?因为不管这些代码长得啥样,但至少是身经百战能稳定运行的,所以要尽可能复用现有代码,并在现有代码上迭代,这种迭代方式是软件工程实践用时间证明比较可行的方案。而且,近些年的IT环境跟传统的开发环境不太一样,传统的产品开发,如果产品死了,代码就死了,基本没有重构的必要。现在的行业环境比前些年前进了不少,代码能复用就复用,甚至有些很小的团队很短时间就能出一款产品,这都是因代码复用的功劳。所以,我们不得不重构。

“脏乱差”的代码都长什么样呢,以及能从中得到什么启示呢?

表现及分析

具体的软件项目涉及方方面面,但项目立足于工程代码,而脏乱差的工程代码直接让开发者望而却步,进而影响各方面工作的进度。我接触到脏乱差的工程代码,主要涉及:

封装差

第一眼看到代码的感觉,形象点,就是洋洋洒洒一大篇,跟医生的开的处方似的,看的云里雾里。

现实是复杂的,处理现实需求的代码也应该是复杂的。这没什么问题,但代码的主逻辑应该是清晰的,举一个好搞笑的例子,宋丹丹的小品:怎么把大象放冰箱?总共分三步:

  1. 把冰箱门打开

  2. 把大象放进去

  3. 把冰箱门带上

这三步构成了需求的主逻辑,非常清晰可见,任谁都能看懂并吸收。其他的,如,怎么让大象听你的话,用多少人打开和关闭冰箱门等等,都是细节,应该封装起来。如果把这些步骤混到主逻辑里去,任谁都看得不明白。我平时接触的代码之所以难读,很大一部分原因属于这类,各种较低层次的细节遍布各处,绕来绕去,实在不堪其烦。

这类问题的解决方案就两个字,封装。把细节封装,使用抽象数据类型把数据结构的细节隐藏起来。

风格各异

每个人的风格肯定是不一样的,所以必须要有规范约束每个人的自由发挥。网上已经有一些不少的公司贴出了自家的规范,比如阿里的Java编码规范,googleC++规范等,都值得参考。当然,如果你们公司有自己的规范,尽量遵循自己的标准。在这里我提几个我认为比较常见且要规范的地方:

  • 标识符的命名规则

    实际上,现在我这边已经混乱了。早先的代码风格是匈牙利命名法(我之前写文章讨论过其缺点,可以参考这里),随后又有Linux C的命名规则(单词之间使用短横线分隔),后来又有驼峰的加入,现在真是百花齐放。

    命名规则还是尽量要统一,否则读代码是不畅快的。

  • 函数声明的风格(Unix vs Windows)

    对比一下创建文件的系统api,Unix和Windows下函数声明的样子:

    // Unix
    int creat(const char *filename, mode_t mode);
    
    // Win32
    HANDLE CreateFileA(
      LPCSTR                lpFileName,
      DWORD                 dwDesiredAccess,
      DWORD                 dwShareMode,
      LPSECURITY_ATTRIBUTES lpSecurityAttributes,
      DWORD                 dwCreationDisposition,
      DWORD                 dwFlagsAndAttributes,
      HANDLE                hTemplateFile
    );
    

    看到没,Unix下函数声明短小简明,最直接的收益就是可读和可理解性较高,使用起来很方便。而Win32的函数又臭又长,想把所有参数搞清楚得费很大的劲。上面的例子反映的是一种常态,大家可以随便找几例对比一下。

    我接触的代码中,也有这种又臭又长的函数声明,10几个参数的函数也很常见, 很难搞明白每个参数的用途。很多参数是指针类型,不同的调用情况,会将相应的参数置为NULL,真是够复杂的。从心理学上讲,人的短期记忆的点的上限是[7-2,7+2]个,10几个参数肯定是不利于记忆的,所以不要把函数参数整那么多,真的不好记、不好理解,要采用Unix下的风格。

  • 注释的风格

    这也很重要。很多公司并没有就这块做出具体规定,导致群魔乱舞。先讲几个坏的例子:

  1. 代码几乎每行都要注释(这是做翻译吗?)

  2. 代码的每次更改的地方都加上时间、作者信息,跟盖楼似的,快超过珠穆朗玛峰了(这是搞注释竞赛吗?)

  3. 代码跟注释不一致(这是典型的心口不一,很常见)

我要表达的核心意思是:尽可能不要注释,用清晰的代码诉说你的意图。这个论点的支撑有以下几点:

  • 注释是额外的纯体力劳动,而且没有工具进行验证,很难保证与代码的一致,这块的维护是很难的,所以越少越好。

  • 过多依赖注释,会降低代码的质量,比如命名的准确性,反而不利于代码的维护。

  • 之所以强调“尽可能”,也得具体情况具体分析,有些事项的确需要做注解,比如紧急项目的凑活代码,但很多都是需要后期处理的,这不与我的观点违背。再说一下上面谈到的第二个例子,这种盖楼我真的觉得没必要,当功能已稳定,这些注解基本没有存在的必要,反而存在本身会带来困扰。而且,现在很多版本控制工具,比如,git都已经能跟踪代码的变化的时间和作者这些信息了,就不要做这种人力维护的事了。

代码冗余严重

冗余的代码给维护设置了一道很高的门槛。比较常见的冗余有两类:

  • 代码copy

    甚至出现过同一个项目存在多份完全一样的代码,而且都在调用。要修改这些函数的行为,需要所有地都要改,无疑中增加了工作量,最要命的是容易忘。

    所以,代码要力求一份。如果同一个工程多份,要删掉多余的拷贝;多个工程共用,最好建一个共享的项目。

  • 相似功能的函数存在多份

    利用函数重载的情况下,同一个名字的函数很多,而且都很长。不利用函数重载的情况下,各种后缀形式的函数争奇斗艳,比如fun_1,这类以数字编号的,fun_ex这类以英文缩写标记的,还有不少是以日期后缀的,很多很多。

    函数重载是多数情况下是必要的,但不要每个函数都是一份完整的实现。采取的原则应该是:核心代码只有一份,其他代码只是对于核心代码调用形式的不同。这个核心函数,可以称之为代理函数。C++11 以后引入的代理构造函数是一个道理。

不使用标准库

很多公司都有自造的轮子来替代标准库。为什么这么做呢,我们来探讨一下。我觉得主要原因是早些年C++演进的速度非常之慢,从C++98到C++11经历了13年,那时候标准库非常薄弱,而C++标准委员会又无所作为,很多公司的轮子差不多是这时开始造的。

当然,这些造轮子的公司也都有自己的说辞,比如,可控性强,因为是自家实现的嘛;性能高,毕竟是自家实现的嘛。。。但现如今这些理由都已经站不住脚了,目前基本上三大编译器的标准库都已经开源了,跟自家的没两样;而且标准库采用了很多优化,真的性能很高;而且,很多编译器对标准库的调试过程中内部结构的查看等支持真的很好,自家的轮子是没有这个待遇的。标准库是同一编程语言的开发者之间的共同语言,可以有效复用以前学习的知识和经验,这是标准库最大的贡献。

所以,要尽可能使用标准库。这些年C++已经改变的很快了,每个版本标准库也在扩充,所以条件允许的话,把编译器升级到最新,把标准库用好。说句题外话,用最新的编译器的好处是新版本的二进制优化更好,曾经我测试一个项目,将vs 2012的编译升级到vs 2017,效率整整提高了40%。

关于RAII

RAII (Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源的惯用法,常用于内存资源、网络连接、文件、数据库等资源的管理。

如果你见过每个if分支里都有关于所申请资源的释放代码,你一定要用RAII技术改造一下,这能大大简化你的代码逻辑,并降低内存泄露的可能性。RAII本质上是通过一个类型自动地管理资源,这是C++相比于C的最大优势之一。

当然在C中也是可以使用goto语句达到类似的效果,但与RAII对比,RAII更安全,更可读。

关于状态

关于状态,我更倾向于无状态的函数,这仅仅是个人倾向,这里简单阐述我的一些看法,不同意也没关系,因为一般情况下不同风格不会带来太大的问题。

无状态的函数基本继承了纯函数的一些优秀的特性,比如,利于并发等等,这里不细讨论。我所关注的点,在于函数的可读性,你能从函数的输入和输出清晰得看出数据的流向。

class Test
{
public:
    Test(int a, int b, int c, int d):a(a),b(b),c(c),d(d){}
    //使用a、b、c、d数据生成一个token
    std::string generateToken();
private:
    int a;
    int b;
    int c;
    int d;
};
namespace Test
{
       std::string generateToken(int a, int b, int c, int d);
}

上面两个例子中的generateToken实现的效果是一样的,但我更倾向于第二种。因为能更清楚地看到函数契约,在未深入读代码地情况下就已经有所认识,而Test类的函数完全看不出这些信息。

今天就写到这吧,希望能给你所有启示。喜欢我的文章,请关注我的公众号。

封面图片使用Abujar Ansari的作品。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园的建设目标是通过数据整合、全面共享,实现校园内教学、科研、管理、服务流程的数字化、信息化、智能化和多媒体化,以提高资源利用率和管理效率,确保校园安全。 智慧校园的建设思路包括构建统一支撑平台、建立完善管理体系、大数据辅助决策和建设校园智慧环境。通过云架构的数据中心与智慧的学习、办公环境,实现日常教学活动、资源建设情况、学业水平情况的全面统计和分析,为决策提供辅助。此外,智慧校园还涵盖了多媒体教学、智慧录播、电子图书馆、VR教室等多种教学模式,以及校园网络、智慧班牌、校园广播等教务管理功能,旨在提升教学品质和管理水平。 智慧校园的详细方案设计进一步细化了教学、教务、安防和运维等多个方面的应用。例如,在智慧教学领域,通过多媒体教学、智慧录播、电子图书馆等技术,实现教学资源的共享和教学模式的创新。在智慧教务方面,校园网络、考场监控、智慧班牌等系统为校园管理提供了便捷和高效。智慧安防系统包括视频监控、一键报警、阳光厨房等,确保校园安全。智慧运维则通过综合管理平台、设备管理、能效管理和资产管理,实现校园设施的智能化管理。 智慧校园的优势和价值体现在个性化互动的智慧教学、协同高效的校园管理、无处不在的校园学习、全面感知的校园环境和轻松便捷的校园生活等方面。通过智慧校园的建设,可以促进教育资源的均衡化,提高教育质量和管理效率,同时保障校园安全和提升师生的学习体验。 总之,智慧校园解决方案通过整合现代信息技术,如云计算、大数据、物联网和人工智能,为教育行业带来了革命性的变革。它不仅提高了教育的质量和效率,还为师生创造了一个更加安全、便捷和富有智慧的学习与生活环境。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值