大约08年,我在一家公司写了大约800行代码。这800行代码可以一劳永逸的替代他们已有的二三十万行满是缺陷的垃圾。


事情是这样的:他们搞了个很复杂的系统,我不想讨论这个系统设计的有多烂了;总之,他们需要搞几百个不同的报文,在网络上传来传去。每个报文长短不一,有的可能只需三五个字段,有的得有三五十个甚至更多。由于设计水平问题,绝大多数报文字段数量都很大(换句话说就是没有什么上下文,一切一切都要放网络封包里)。

C++数据结构是不能直接在网络上传输的;尤其这个系统有C++写的部分,也有java写的部分。因此必须先转换成网络报文,然后才能提交给网卡传输;等对方收到报文后,必须解析这个报文、识别它的数据结构、然后在把它转换回来。

报文类型太多,他们的程序员能力又……嗯嗯,稀烂;所以他们决定,所有这些数据结构都要转换成XML传输,对端收到后,再解析XML、还原数据。

总之,一来一回,他们这每个报文往往需要少则两三百行、多则三四千行代码,这才能完成数据收发工作。几百个数据结构综合起来,代码量轻松破几十万。


总之,因为程序员能力问题,他们这个系统的水平啊……真是一言难尽。

只说一点吧:他们居然把debug版的assert重新定义为空,因为……他们害怕崩溃。

可是,掩耳盗铃就真能治崩溃吗?

有一次,一位同事愣是跨越不知多少层逻辑,把自己的XML灌进了我的执行栈。

程序崩了,找我;打开core一看,我的程序栈被灌了一个一塌糊涂,内容是一个八竿子打不着的XML。我就问这个表格是谁负责的,这才找到他。

然后,这位找到一个一千多行的函数,前面几十行拷到末尾,末尾几十行拷到前面,中间再呼啦删掉几十行……折腾大半个小时,告诉我“好了”。

一运行,程序崩了。崩在他的代码里面。

当时一位和我关系很好的、华为出来的同事实在看不下去了:“你写程序都碰运气的吗?来,看这字符串内容,看它和哪个变量有关……排错是需要逻辑的!哎呀你怎么不用循环?算了算了先排错……”

这位还一脸不忿:“看看!编译错误!我的起码能编译!”

没理他。五分钟后,问题解决。

和另一个组同时接的一个任务,我们一周完成,然后找经理要第二个、第三个任务,等做到第四个甚至第五个任务了,这个组终于姗姗来迟,完成了自己的任务——然后装的很努力的样子,星期天到公司加班联调。

然后,他们十万火急的喊我去公司,说我程序有bug;问是什么bug,不答。过去一看,需求没写清楚,只说某个字段是一个字符串;我按C惯例,后面留了个;他们认为这里不应该有,五六个人就干等我过去……

删掉,数据传输正确,对方的模块立刻崩溃。检查再三,数据没问题,他们自己写出bug了——这个谁负责的?他来了没?出差了?那调不了,大家先玩吧。

就这么个工作态度。

可想而知,这个系统会是什么水平。


总之,不吐槽他们了。先解决问题。

这个问题我是这么分析的:

1、基本数据类型有限

事实上,每种不同的数据类型,打包/解包流程全都一样。因此不应该每个报文从头敲代码,重复劳动太多了。

如果把它们写成诸如int2xml/xml2int、str2xml/xml2str,那么代码量就会大幅降低。或许不到十万行就能解决问题——等于代码量下降到原来的1/3甚至1/4。

事实上,C++支持泛型。完全可以统一搞成个var2xml/xml2var,类型推导系统会自动推导出正确类型、生成正确的代码——至多针对特殊数据类型做一点特化。

这样基础数据的打包/解包操作会更简单,代码量可以进一步降低——因为无需判断数据类型了,对每个字段调用var2xml(s.item, str_buf, buf_len)就行了,泛型系统会自动选择正确实现。

用这种方法写,或许三五万行代码就能解决问题。

2、所谓数据结构,其实不过是基本数据类型的组合

因此,如果可以让程序“知道”某个数据结构里面的每个字段的偏移位置以及数据类型,那么完全可以统一处理所有报文。

比如说吧:

struct login {
   char username[20];
   int ID;
}

如果程序能从login这个类型,知道它的开头是20个字节的char数组、然后第21个字节开始是一个int的话,自动从login转换成xml报文或者从xml报文重建login,就是很简单的一件事了。

(用size_of就可以知道int的大小,这也方便不同平台之间迁移。如果需要固定字节数,可以声明为int_16/int_32等。)

但是,C++本身是支持不了这个的。怎么办呢?

学MFC,玩宏:

DECLARE_TABLE(login)
DECLARE_TABLE_ITEM(login, username, char, 20)
DECLARE_TABLE_ITEM(login, ID, int)
END_TABLE_DECLARE(login)

用这个方式接管struct声明过程,我们就可以在宏里面玩猫腻了。

我的做法是,DECLARE_TABLE里面其实没有生成login这个struct,而是声明了一个login_details的数组;然后DECLARE_TABLE_ITEM其实是在初始化这个数组,把不同偏移位置的数据类型记录下来。

直到END_TABLE_DECLARE这行,login这个struct才真正建立。

然后就简单了。写一对模板函数struct2xml/xml2struct,利用模板推导,自动查找typename##_details数组(这个##是gcc的扩展,用来拼接字符串),关于typename的细节就到手了。然后逐项处理这个数组,自然就完成了打包/解包工作。

这个东西类似于后来名声显赫的protobuf;只是我那时技术视野还不够宽广,仿效对象是不够优雅的MFC。因此缺陷颇多,看起来诘屈聱牙,还用了gcc的关于宏的一些非标扩展,较难维护。等后来见了ORM和protobuf的实现思路之后,我才知道自己的笨拙。

它一共花了我两三周时间,写了800多行代码。照例,一次编译通过,测了一周,完美支持各种报表数据;而且在设计之初就选用了效率最高的方案,0额外内存占用,也没有额外的读写负担。

现在,只要把原始的结构体声明替换成这么一组DECLARE宏(可以写一个小程序转换),然后需要xml时调用struct2xml,需要从xml还原时先识别报文类型再调用xml2struct——还是那句话,无需区分,无需记忆,类型推导系统会自动帮你选择。

现在,让我们对比一下。

过去,为了打包/解包这些报表,按共200个报表(其实还不止这么多……人家的设计就是信手画个蜘蛛网,一个心跳处理都能画十几个方块、几十条线路,包含若干种不同报文)、每个报表有二十个字段、每个报表打包解包一两千行算(因为每个字段都要复制,要在xml中记录和校验长度,要写日志方便追查执行流程;而且网上收到的每个xml报文都需要先解析xml、确认每个字段名称;加上他们很多人不用循环,写的又臭又长,每个字段用五十到一百行代码完成打包/解包并不算多。注意注释也算代码行数),共需20到40万行代码。

四年共1460天,208周;那么按三十行代码算,一个人必须每周开发1400多行代码,才能写完这些代码。

然后,这每周1400行代码,又得多少测试,才能堪堪够用呢?

要知道,很多名校毕业生,写一两千行代码就得两三个月;然后为了测这个代码,又得来来回回反反复复折腾两三个月甚至大半年的,就这样都还bug不断。

哪怕这1400行代码因为太过平铺直叙所以成了熟练工的体力活,起码也得另外的一周测吧?

要按之前那个拷来拷去排错流搞法,那可不是另外一周。那是一周测出N个bug,每个bug都要花一两周定位到人、然后再花个把月修复……然后再修出新的bug,在解决新的bug的过程中,旧的bug又莫名其妙的回来了……

别笑。这些人就是这么工作的。所以这个项目在我加入时已经被200多人开发了一年半——但里面毛病太多了。因此直到我离职,仍然没有半点眉目。

而我这800行代码,写完再不用动。想找出所有bug,是不是容易太多?

然后,随便你想个什么报文,用DECLARE宏一声明,struct2xml然后发送,或者先接收、识别报文类型然后xml2struct解包……搞定。回家爱干嘛干嘛吧。

你看,这只是编程语言基础知识的一点小小应用。会了,你就可以955、然后用三周时间的800来行代码,碾压别人007连轴转、每周1400行代码忙碌四年的成果。

而且,800行代码可以一次写出0bug;每周1400行代码写四年,又岂是另外一个四年能抓尽bug的。

你省心省事,老板得了实惠——别人上千万都搞不定的,到你手里一个月不到搞定,帮他一下子省了99.9%的支出,又帮他抢在潜在竞争对手之前搞定上市:你觉得这东西应该值多少钱?一个能带着整个团队、以最有效率的方式直线达到目标的技术专家,相比于那些“一将无能,累死千军”之徒,哪个用起来更合算?

当然,这只是个简单案例。因此一个基础知识掌握较好的“仅仅”以5、60倍的效率碾压了那些基础知识没有很好掌握的。稍微复杂一些的情况下,好程序员效率百倍于差程序员的情形,都是业内人士司空见惯的。


2020-9-3补充:

玩过的都知道,XML封装起来很方便,但解析嘛……

要么,解析库会先跑一遍,把它整成一棵“树”,然后用户按需读取每个分支、每个节点(但有时存在一些数量不定的数据,比如携带的某种数据可能是0条、1条或者N条,因此必须按一定顺序读取)。

这个做法直观,方便什么都不懂的初学者使用,但效率较低。因为要先开辟内存、解析XML填充树结构,然后再访问这棵树,访问完了再删除树;等于多了至少一次内存分配/归还操作、多了一遍为了建立解析树的读写操作、也多用了一些内存。

要么,库识别tag,你在回调里按顺序边读边解析边填充到C结构里。

这种效率更高,因为只需访问一次;但必须自己注意嵌套结构何时出现、到了第几层、遇到结束符如何返回,等等。它需要你熟知各种基本算法,对技术不高的初学者来说难度较高甚至无法理解;对学艺不精的工程师来说较为复杂,一不留神就会出错。

顺便的,我看到这个回答下有人认为这和“计算机基础知识”无关。嗯……我不知道他是业界大佬呢,还是单纯的看不懂……

可为什么之前我回答这个问题,又引的一票子人跳出来说我理想说我低端呢?

为什么他们暴跳如雷呢?

因为他们觉得leetcode的这种题目钻牛角尖、过难、无聊:

恐怕任何懂点编程技术的都不会觉得这种题目配叫hard吧

这不是想都不用想的基本操作吗?我平常用太多以至于都忘了这居然也配叫算法了

别说这种题目太简单以至于我不觉得它配叫算法;其实我搞这个东西,自己都没觉得它用了什么算法。对我来说太简单太不值一提了。

倒是很多人做不到、只会出苦力,这反而令我惊讶。

可是,这种难度的leetcode题目你都搞不定的话,我这随随便便的、一边解析XML文本树、一边借助另外一套数据结构从XML重建C struct的混合操作(而且这套数据结构还是借助简单的宏入口自动产生和填充的、可以自动适配任意网络封包),你怎么可能看懂?

实实在在的、简单纯粹的树你都搞不懂;它可是针对尚不存在的、用户自定义数据结构而编写的、自动把用户数据结构序列化成树以及自动从序列化状态的树中解析重建用户数据结构的一套东西啊。

你起码得先能想象它如何处理某个特定的数据结构,这才可能看懂它的序列化/反序列化思路;然后你还得能搞明白遇到不同的数据结构它会有什么不同表现,这才能真正读懂它。

这东西,对我的确不难。容易到我在回复中都认为它仅仅是“玩转了语言基本概念”而已,根本不觉得它涉及任何算法问题。

但是没想到,它居然包含不少leetcode上足以称为hard++的算法——那些比它容易得多的算法居然都会被很多人称为“钻牛角尖”。

如果那居然都能吓倒你、让你觉得那是“屠龙技”;那么这种被我当成不值一提的“杀狗术”、随随便便拿出来就用了的东西,你该如何对待?

我在会议室侃侃而谈时,你怎么可能不打瞌睡?

别说通常开会时,我谈的都是还没写出来的、自己认为可行的思路了;对着写出来的这800行代码一行一行讲给你听,你能听懂?浪费彼此时间而已。

这种情况下,你怎么可能不遭遇中年危机?

出苦力你一身职业病,比得过精力充沛的小年轻?

出巧力……你摸得到门槛吗?要知道这东西我可真没觉得存在一丝技术含量在里面,全都是自然而然想到就写的东西;结果比这个容易得多的leetcode链表题,在你们眼里居然是钻牛角尖的、不事先背题就没人能搞定的屠龙技?那当我们讨论我们眼中的难题时,你站哪儿合适?哦不,你适合进会议室吗?那你转的哪门子管理?你想管谁?你有资格管谁?

因此,他们在的地方,我不会去。丢不起那人,也憋屈。

而我去的地方,他们去不了。别说跟上节奏了,打个下手做个测试都不够格。

连我都跟不上……须知比我强的人,可谓车载斗量。

你看,这就是计算机基础知识的重要性。这就是为什么我敢说“不懂基本算法的根本就是滥竽充数的南郭先生”的原因。

文章来源于互联网:计算机基础知识对程序员来说有多重要?

发表评论