编程的技术|艺术|术术——面向开发者编程

 

        同学们好,我叫梁峻墅。今天想和大家分享的话题是:编程的技术|艺术|术术。这里的“术术”你可以理解为哲学。

        面向开发者编程,与大家平常经常说的面向过程编程,面向对象编程等不太一样。我理解我们程序员编程面向的对象是人,是程序员,是开发者。用户并不看代码,看代码的都是我们的同行,所以我们要多一个面向开发者的思维角度来看待编程。

骨灰级程序员

        我从15岁开始编程,现在已经整整编了35年了,算是骨灰级的程序员了。我小时候第一个编的程序是游戏,我相信很多程序员最开始编程都是从兴趣爱好开始的。我当时就是觉得别的游戏不好玩,自己编更好玩的。那个时候是80年代,最牛逼的电脑也就是苹果的Apple II,64K内存。而我们用的都是娃娃机,只有几K内存。所以那个时候编程序,都不是逐字节,而是逐位逐位地抠内存,考虑着怎么省,宁可用复杂且不易理解还容易出错的位运算,也要用1个字节的8位分别表示8个布尔型变量。到了现在,在设计数据库的表状态字段时,还习惯性地使用位表达,而不是连续自然数。那个时候的存储连软盘都没有,更别说什么硬盘闪存了,用磁带都是高级货。但家里录音机的磁带质量又不够好,经常存上去捞不回来。最后为了玩游戏,开机后都要自己把游戏代码再敲一遍,才能玩。这个开发环境真是惨绝人寰!但正是在这种艰苦的环境下,反而锤炼出在技术上精益求精的精神。一方面,为了能早点儿玩上游戏,不断地优化代码,愣是把200行BASIC代码优化到80行。另一方面,打字的速度也快速提升,没学过指法,但上大学时学五笔字型,我一个指头戳都比别人快。所以环境不好,不一定是坏事。现在的编程软硬件环境比以前好太多了,但很多程序都编得更加随心所欲了,内存分配都是大手笔,企图靠硬件来掩饰软件的问题。所以大家要珍惜自己当下编程环境,少抱怨,多用困难锤炼自己编程的能力。

        1990年,我上了大学,终于能接触到真正的计算机了,图书馆里有几十本关于计算机编程的书籍被我常年霸占。那个年代电子游戏厅如日中天,里面有很多赌博游戏。正好有个同学的亲戚是开游戏厅的,我们趁他值夜班拆开游戏机,把里面的EPROM拿回学校用单片机仿真系统复制,但一看游戏机主板上的CPU,大家都傻眼了,是6502,APPLE II的CPU,这种高级货学生机房是没有的。幸好我们学校也是当时中国的13所重点大学之一,有多个国家重点实验室,我们在里面找到一台原子分光光度计,它的控制主机是台APPLE II,冒着被开除的风险,拆开主机,换上复制出来的EPROM,用APPLE II反汇编机器码,并打印了500多页的汇编程序,我们在针式打印机的怒吼中提心吊胆的过了一宿。最后愣是靠目视代码,从两万多行代码中找到了只有8行代码的变牌算法,然后横扫那个城市所有的游戏厅,后来游戏厅老板见了学生摸样的人都不让进。有些执着的同学干脆学也不上了,坐着绿皮火车逢站必下,扫荡了周边一个又一个小县城……但我们那时候还不知道有黑客这个词,认为破解系统是天经地义的,就像编程技术精益求精是理所当然的一样。

        大学毕业后正赶上中国IT方兴未艾的时代,我搞的是单片机控制系统,还有MIS系统,就是信息管理系统,现在叫ERP,编程的范围越来越广泛,使用的编程语言和开发环境越来越多。没过几年,就到了上世纪末,中国迎来了互联网时代。1997年,我作为古城热线的第一批互联网用户,用19.8k的猫拨号连接到互联网上,那是激动万分,终于可以了解世界上其他程序员是怎么编程序的。同时,也了解到编程的顶级精英被称为Hacker,但中文被翻译为黑客,带点不正经的味道,后来在不理解编程技术的媒体推动下,这个词已经成了不折不扣的贬义词。到了本世纪初,电信数据增值业务,简称SP突飞猛进,像打开的潘多拉魔盒,放出无数洪水猛兽,大量技术人员天天琢磨着怎么让你回条短信就扣掉2块钱,再顺手给你订个15元包月业务,就是无差别抢劫。那个年代是技术人员的高光时刻,充分体现了那个时代的中国式标语——科技是第一生产力。黑客这个称谓已经不足以平民愤,直接叫白领黑社会。行业监管到位后,大家终于可以心平气和地干点正经事了。照抄美国,开始进入到虚拟化、云计算的业务领域,然后就是搞机器人和人工智能。现在的技术理念和手段越来越多,信息量越来越大,人们越来越急功近利,软件不行就用硬件堆,仅仅是为了完成交付,能懵就懵,能骗就骗,世风日下,很多顶级程序员的光辉思想都被淹没在星辰大海中。

        现在的编程越来越没啥挑战,几乎所有的积木都基本造好了,绝大多数程序员都是在大量堆砌积木,稀里糊涂地就把功能完成了,如果没完成那就换块积木,没人仔细研究为什么。远没有小时候那种控制机器,挖掘机器潜能的乐趣。但不可否认,编程门槛的下降也繁荣了整个软件行业的生态,而开源的出现,能够促进代码从市井般的粗俗进化到贵族般的优雅。所以,现在的我在做一些开源的相关工作。

程序员文化和武侠文化

        我理解程序员文化与中国的武侠文化有很多相似的地方。武侠经常提到在密林峡谷中练功修行,内力练到位后,一出江湖,就是血雨腥风。很多程序员年轻的时候也一样,研究代码经常废寝忘食,吃住在公司,回家就是为了洗个澡。每天晚上干到2、3点,经常把爱因斯坦的话挂在嘴边:“成年人睡4个小时就够了。”这些其实也是在修炼武功。我大学刚毕业的时候,Windows才3.0吧。那个时候没有互联网,MSDN也还没有,到哪里去找编程资料呢?所以大家手里都有一个API的手抄本,很多API的调用都是自己试出来的。很多参数也不知道是啥意思,就用穷举法一个一个地试,去探索,去发现。所以大家每次见面都会炫耀一下,这个函数你会调吗?一个小本,很牛逼的样子,就像武功秘籍一样。

        再往后,微软出了MSDN,每期至少2张光盘起。被中关村的不法商贩看中了,就用MSDN的封面给日本动作片光盘套壳来躲避稽查。大家买到这样的MSDN,都会体谅不法商贩的良苦用心,默默地收藏起来,然后怀着忐忑的心情继续去淘MSDN,即希望买到真的MSDN,又希望不是。但不管买到啥,都有收获,所以程序员很早就有了平和的佛系心态。那个时候的MSDN,就是程序员的《葵花宝典》:一书在手,天下我有。想查什么随便查,想调用什么就去搜,内容详尽,还可以通过超链接无限拓展你的视野。那个时候已经不是会不会调用API的问题了,而是能知道调用哪些API能让你的程序更加精简、稳定。尽管大家常说,不要重复造轮子,但我想说这个比喻真是丧尽天良!程序员面对的绝不是使用几个轮子的造车游戏,而是使用成千上万个积木的拼图游戏,已经远远超出了正常人类的脑容量。由于API实在太多了,而且还在快速迭代,任何人都很难笃定哪些功能已有现成的API,哪些功能还没有。所以大家开始比拼的是知识面有多广,再后来就变成了玄学,根据常年跟微软程序员了解的调性,设身处地地猜他会怎么思考这个问题,我们怎么做才能成本最低。

        随着API越来越多,软件规模也象滚雪球般越来越大,像求伯君老前辈那样凭一己之力写个字处理软件的时代一去不复返了。尤其是系统级的大型软件,需要很多人联合开发,就不可避免要去修改别人的代码。这个时候武侠的情节又出来了,同级别的程序员基本上都看不上对方的代码。你看看东邪西毒南帝北丐,谁鸟过谁?别人代码拿过来只是用旁光看一眼,然后就是“重构”。其实,对于中级程序员而言,写代码并没什么难度,想清楚编程思路之后就是体力活,所以这不算什么本事。但修改别人的代码,可要比自己写要难的多得多。首先别人的代码就像个黑盒子,你得一点一点地通过阅读代码,让大脑像计算机一样运行一遍别人的程序,还要抽象出来别人的设计思想;然后再把自己的思想和别人的思想进行巧妙整合,和平共处;最后还要对代码进行一次精准的外科手术,才能实现自己的目标。这绝对是个脏活、累活,但在外行人眼里,觉得你不就改了几行代码吗?很多程序员在按代码行计算工作量的世俗傲慢与偏见下,选择了用愚蠢对付愚蠢。而那些有思想的牛逼程序员不为所动,不断通过修改别人的代码,提高自己认知的边界,逐渐成长为一代宗师。而总是自己写代码的程序员,则永远在自己认知的范围内低水平徘徊。程序员的三六九等就是这样被拉开的。

        等到了软件开源的时代,开源代码就更需要看其他人写的代码了,我理解这是编程技术追求更高更远的必然结果。在开源时代,参与代码共建的程序员之间必然产生共情,了解对方是怎么想的,理解人家的思路。但对于大多数人而言,理解他人是很难的,每个人都觉得自己是最正确的,这是人性的弱点。而开源能够让程序员的心胸和视野更加的宽广,把理解他人作为一种编程习惯,这种境界就不一样了。当然,开源代码看多了,可能武侠精神又会出来了,这怎么写的这么烂,还不如自己的代码开源呢。这个呢,就有点反武侠了。中国武侠,武林秘籍,都是自己门派的独门秘技,绝不外泄,不能让别人看。而开源代码,相当于把武林秘籍公开了。但我理解,看到源代码,到真正理解源代码后面的思想,还有好长一段距离。而且还要看你的段位,就像九阴白骨爪第一式,气沉丹田,你得先知道丹田是啥,在哪儿。还有欧阳锋练蛤蟆功走火入魔的呢,显然没理解对代码和思想的差异。所以开源代码并不一定能让所有获得的人真正获得,而真正获得的人就是你的同道中人,你就不那么寂寞了。开源时代之前,很多老板以为掌握了源代码,就是掌握了核心技术。而真正的核心技术其实是思想,而思想从来都是由人来掌握的。李白曾经写过一首《江湖行》,我就结合开源,共建一首《开源行》:

 编程的思想境界

        说了这么多,主要是为了给PUA做好铺垫。现在进入正题:编程的终极奥义到底是什么?我理解只有思想恒久远,代码才能永流传。因为编程就是把人类的语言给转换成计算机能执行的机器语言,本质就是一种翻译工作,但翻译的水平也是有高低贵贱之分。像电影《Sucker Punch》是一部非常有创意的内涵思想片,还能融合梦幻、魔幻、科幻、暗黑、伦理、动作、微色情、小萝莉、暴力美学、解谜等多种元素,还能做到雅俗共赏,不同层次的人都能从这部影片中获得不同的乐趣。我还专门给这部电影做了一个暴力美学特辑(Sucker Punch——美少女特攻队(暴力美学特辑)_哔哩哔哩_bilibili),有兴趣的同学可以去看看。但这部电影在华语区的票房惨淡,就是由于译名问题,该片的价值被严重低估。台湾翻译为《杀客同萌》,基本做到了意音具备,就是神还差点。但看看大陆翻译为《美少女特攻队》是几个意思,让人以为是二次元动画片。再看看香港的翻译《专扁衰仔》,你就知道没文化有多可怕,扎克·施奈德估计直接哭晕在厕所里。翻译的思想高度不同,对作品的理解深度不同,翻译的质量是天壤之别。而对于像编程如此缜密的翻译工作,那更是需要蕴含深刻的思想才能做得更好。

        既然谈到思想,就要谈到思想的境界。我理解编程的境界有四层,借用孙子兵法,分别是:下兵伐城、中兵伐谋、上兵伐心和不战而屈人之兵为超然

        第一层——下兵伐城:很多程序员,一接到编程需求,就立刻启动开发环境,开始编程敲代码了。这就是没什么想法,先干起来再说,编到哪儿算哪儿。面向交付编程,只为完成功能,头痛医头,脚痛医脚。一路攻城略地,好不威风,但这只能算初级水平。

        第二层——中兵伐谋:拿到需求之后,不着急动手,先开始思考,开始谋划,应该走什么技术路线,结合哪些已经做过的代码,还应使用哪些技术手段来快速、轻松地完成这个需求。这个层面的程序员已经开始不靠纯技术硬扛需求,而是开始艺术性地解决问题。大家拼的不是能不能解决问题,而是看谁解决问题的成本最低。像中医那样,并不直接治病,而是阴阳调和,打通经脉,调动自身免疫系统,四两拨千斤,这已经开始有中国式智慧的味道了。这种人经常在计算机旁边呆坐半天,即使表面上在和你说话,但其实脑子里在伐谋呢,所以经常表达古怪,被认为是书呆子。

        第三层——上兵伐心:这种人已经能看到文人的清风傲骨,在更高的战略层面思考解决问题的时空成本,包括但不限于时间成本、分析成本、设计成本、编码成本、调试成本、部署成本、维护成本、用户的使用成本、资源的调配成本、社会的综合人文成本等等,得到结果后会产生一个灵魂拷问:“为什么要解决这个问题?能不能不解决?”带有原罪的需求都挺不过这个终极审判。比如很多需求,只是貌似合理,但实际上是伪需求,顶尖的高手能够抽丝剥茧,发现需求的逻辑矛盾和漏洞,并能综合运用各种手段,甚至包括非技术手段来及时纠偏。比如做MIS系统的时候,客户方的办公自动化系统需求很简单,就是把现有的管理规章制度全部用计算机程序实现一遍。这个需求貌似天真无邪,但实际上它忽视了人机之间的差异和特点。比如对人工作业,为达到作业合规的要求,管理规章制度中就要有反作弊措施,各个相关部门还都要有作业数据备份,以便将来发现问题倒查时可以对口供。但对IT系统,用户都有登录ID,作业都有事务ID,交互都有会话ID,存储都是一体化的,根本不需要那些画蛇添足的步骤。顶级的程序员就会庖丁解牛般地优化管理流程,引导需求合理化,节省了大量的研发资源、应用资源和社会资源。可能只需要修改管理规章制度的几十个字,就能少写几万行代码,bug能少几百个,部署维护能少几个月,用户每天能少点击数千次鼠标。通过少编程,甚至不编程就能解决问题,才是最牛逼的境界。

        终于来到最最牛逼的境界——不战而屈人之兵。前面三层境界,也就是在满足需求、预测需求、引领需求上做文章,毕竟都是咱们这个世界可以理解的事。而最高境界已经跳出三界之外,不在五行之中,已经无法理解了。

        看个场景先:小白最近代码输出太少,说机器频繁死机,耽误了工作。大牛对着机器,上去就是一脚,从此再不死机了…天理何在?小白不服啊,碰上死机的机器也来一脚,结果当天就去财务领工资了…天理难容!虽然都是人,但差别就这么大,找谁说理去。

        再看段对话:

        小白:“哥,我做的那个模块总是有bug,调试两三天了,一直找不到原因,您有空帮我看看呗。”

        大牛:“go”

        来到现场。

        小白:“您看这…”

        大牛:“别急,从头开始把错误给我演示一遍。”

        小白:“好嘞!”

        一个小时过去了…

        小白:“我call,怎么不出来了呢?大哥,我发誓我找你之前还复现过一次错误。”

        大牛:“没事,我已经习惯了,等再出错,保留现场,call我。”

        然后就没有然后了,因为再没出过错!

        大牛只要头皮更出众,那就bug去无踪,所有的大牛都有这样的体验。我理解大牛都是有超自然的气场,常年抓bug,戾气太重,bug都吓跑了!不管你们信不信,我反正是信了。这就是最高境界,什么都不用干,坐在那儿喘气就能解决问题…

编程的技术

        段子讲完了,现在咱们来谈点真正有用的编程技术。编程有着悠久的历史,大约在137亿年前,奇点大爆炸,咱们人类不幸处在这个由时间和空间组成的万恶世界,空间可以理解为除时间之外的一切,时空之间不断相互转换,对称规则让整个世界动态守恒。闲的蛋疼的意识们企图把这个混沌的世界按自己的想法有序化,世界则按规则自动产生了无序与之平衡。说这么累的意思就是,程序员编程的一生都要与bug为伍,不要害怕它,也不要想着完全消灭它,而是想办法与它和谐共处,人bug合一。第一就是尽量减少bug产生的机率。毕竟人少了才好相处,人太多了则很难伺候。谈过女朋友的,应该都有体会。你每敲一次键盘,都有可能产生bug。所以,每次敲代码时,应该多想想自己的祖国、各族人民的福祉,董存瑞、刘胡兰等民族英雄的期许,还有自己父母的厚望,这次键击是不是真的对的起他们!都说头上三尺有神灵,但咱们程序员头上三座大山全是虎视眈眈的bug,你稍不留神,它们就会顺着键盘潜入到你的代码里,且按且珍惜。第二,让bug举步维艰,无处遁形。败兵先战而后求胜,胜兵先胜而后求战。经常打败仗的人总是先打再说,然后企图侥幸取胜;而常胜将军都是先不断创造胜利的条件,已经胜券在握了才开战!编程的基本技术就是要编写低bug代码,一切为了debug,所有的代码都要为调试做好准备。说这些战术级技巧的理论只会隔靴搔痒,后面会结合具体的代码讲解,大家才会更有感觉,点到为止先。

编程的艺术

        其实任何一门技术,上升到一定层次后,都会变成一门艺术。编程也是如此。我们在这个时空世界里,都有自己的当前态和目标态,但无论哪种态都有自己的时空坐标,代码就是企图让这两个态的坐标重叠,而实现重叠的路径是无穷无尽的,但一定至少有一个最佳实现路径。代码的艺术,就是实现这个路径的策略,在空间与时间之间做权衡,要么时间换空间,要么空间换时间,变换到下一个态,不断重复这个过程,直到到达目标态。换成人话就是:所有的代码客观上只有特点,主观上的优点和缺点其实都是程序员意淫出来的,优点都是用缺点换来的,缺点都是优点导致的,优点和缺点实际上是一回事,只是意淫角度不同而已。

        比如,总有人说UNIX/Linux比Windows更安全,而这根本不是技术的问题,而是艺术的问题。UNIX/Linux是个悲观型操作系统,假设用户都是坏人,系统默认什么都不能干,除非明确指出这个用户能干什么;而Windows是个乐观型操作系统,假设用户都是好人,系统默认什么都能干,除非明确指出这个用户不能干什么。所以在默认情况下,UNIX/Linux适合做机机交互,典型应用于服务器,而Windows适合做人机交互,典型应用于个人计算机。但这些特性都可以通过配置安全选项,把Windows操作系统变的比Linux/Unix操作系统还安全,也可以把Linux/Unix操作系统变的比Windows操作系统还不安全。这都是艺术的事,与技术无关,但比技术还重要。

        代码的艺术性范围很广,包括但不限于默认值、参数、接口等具体设计,这些都可以在后面的代码讲解中让大家好好爽一把。

编程的术术

        艺术是技术的抽象,术术则是艺术的再抽象,这部分内容我将尽量说人话。

        我理解程序员在编程的时候,要意识到这些代码是面向开发者的,面向你的同行,是给他们看的。代码在举手投足之间,要透露出一种优雅的美,就是人见人爱,花见花开,车见车爆胎的那种美。

        举个例子:在做一个功能时,发现微软提供的系统API在某些特定条件下,返回值有错误。怎么办?

        基层小鬼:匹夫之勇,自己重新实现一遍该API的功能,结果是除了测试路径能覆盖的条件外,其他条件下大部分是bug。

        中层干部:骚人墨客,给微软报告这个bug,等待官方临时解决方案,合规但官僚。

        高级货:清风仙骨,给微软报告这个bug,但顺手写下解决方案。前置侦测代码,检测此API是否已修复这个问题,如果已修复则正常执行,否则执行纠偏代码。这样无论是现在还是以后,这段代码都可以稳定正确地运行!万花丛中过,片叶不沾身,这就是优雅。

        终极杀手:不讲武德。还记得前面说的那个靠喘气就能解决问题的主吗?他会怎么解决?他就不解决!对,你没看错。他选的技术路线都是用5到10年前的API,坑都已经被小白鼠们填平了,他压根儿就碰不上这种破事儿。最优雅的美就是你看不出他哪儿美,但他就在那儿闲看庭前花开花落,漫看天外云卷云舒。

编程的示例——对着代码理解编程的哲学

        务虚的事都讲完了,现在得真的要讲讲务实的事了。前面讲的那些是武功秘籍的目录,而真正的武功秘籍在代码里。实践出真知,只有虚实结合,才能感同身受。我找了一段0day组织几乎每个程序都要用到的一段代码作为示例。0day,用过盗版软件的朋友应该都很熟悉,它是全球最牛B的盗版组织,里面高手如云,都是Richard Stallman的追随者。任何一个被他们盯上的大厂软件,只要敢早上发布,中午的发布会招待宴还没吃完,破解版就已经在各大盗版网站上可以下载了,平均破解时间就是两三个小时,承诺破解时间不超过24小时,所以叫0day,当天解决,童叟无欺。我们就来看看这些全球顶尖黑客是怎么写代码的。

        这段代码的功能很简单,就是一个基于文件的记录日志类,其C++版本加上头文件,总代码行数不超过200行,而核心代码不到100行,但就在这方寸之间,隐藏着11个战术思想,3个战略思想,还有3个核弹级战略思想。就是个日志文件功能,如果是你设计,能有什么想法?而往往是简单中蕴含的伟大,才能更加让人震撼。现在咱们就按图索骥,开始一段与顶尖高手同行的代码探险之旅。

        这段代码是个标准的C++类,为方便演示,我使用的是其Windows平台的版本,此类可以在所有Visual Studio的C++应用中使用,就两个文件:LogFile.h和LogFile.cpp。

        可以先总览一下:

        LogFile.h

#if !defined(AFX_LOG_H__512AFEC0_D4E8_47F0_AB0C_4E29DAB9A9FC__INCLUDED_)
#define AFX_LOG_H__512AFEC0_D4E8_47F0_AB0C_4E29DAB9A9FC__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <io.h>
#include <tchar.h>
#include <Windows.h>

#define MAX_LENGTH_CONTENT_PER_LINE	1024

class CLogFile {
public:
	CLogFile(LPCTSTR pszPathName4User = _T(""));
	virtual ~CLogFile();
	bool Record(LPCTSTR pszFormat, ...);

	bool SetPathName4Host(LPCTSTR pszPathName4Host);
	bool SetPathName4User(LPCTSTR pszPathName4User);
	bool SetFileName4Host(LPCTSTR pszFileName4Host);
	bool SetFileName4User(LPCTSTR szFileName4User);
	bool SetHeader(LPCTSTR szHeader);

	LPCTSTR GetPathName4Host();
	LPCTSTR GetPathName4User();
	LPCTSTR GetFileName4Host();
	LPCTSTR GetFileName4User();

	LPCTSTR GetPathName();
	LPCTSTR GetFileNameFullPath();

protected:
	SYSTEMTIME m_tSystemTime;

	TCHAR m_szPathName4Host[MAX_PATH + 1];
	TCHAR m_szPathName4User[MAX_PATH + 1];
	TCHAR m_szFileName4Host[MAX_PATH + 1];
	TCHAR m_szFileName4User[MAX_PATH + 1];

	TCHAR m_szPathName[MAX_PATH + 1];
	TCHAR m_szFileNameFullPath[MAX_PATH + 1];

	TCHAR m_szHeader[MAX_LENGTH_CONTENT_PER_LINE + 1];
	TCHAR m_szLine[MAX_LENGTH_CONTENT_PER_LINE + 1];

	bool IsPathOrFileExist(LPCTSTR pszPathOrFileName);
	bool BuildFilePath();
	bool BuildPathAndFilePath();
};

#endif // !defined(AFX_LOG_H__512AFEC0_D4E8_47F0_AB0C_4E29DAB9A9FC__INCLUDED_)

        LogFile.cpp

#include "LogFile.h"

#define ZERO_MEMORY(p) memset(p, 0, sizeof(p))

CLogFile::CLogFile(LPCTSTR pszPathName4User) {
	ZERO_MEMORY(m_szPathName4Host);
	ZERO_MEMORY(m_szPathName4User);
	ZERO_MEMORY(m_szFileName4Host);
	ZERO_MEMORY(m_szFileName4User);
	ZERO_MEMORY(m_szPathName);
	ZERO_MEMORY(m_szFileNameFullPath);
	ZERO_MEMORY(m_szLine);
	ZERO_MEMORY(m_szHeader);
	_tcscpy_s(m_szHeader, MAX_LENGTH_CONTENT_PER_LINE, _T("F0\tF1\tF2\tF3\tF4\tF5\tF6\tF7"));

	::GetModuleFileName(NULL, m_szFileNameFullPath, MAX_PATH);
	_tsplitpath(m_szFileNameFullPath, m_szPathName4Host, m_szPathName, m_szFileName4Host, NULL);
	_tcscat_s(m_szPathName4Host, MAX_PATH, m_szPathName);

	if (pszPathName4User) {
		if (*pszPathName4User) {
			_tcscpy_s(m_szPathName4User, MAX_PATH, pszPathName4User);
		}
		else {
			*m_szPathName4User = _T('.');
			_tcscat_s(m_szPathName4User, MAX_PATH, m_szFileName4Host);
		}
	}

	BuildPathAndFilePath();
}

CLogFile::~CLogFile() {
}

bool CLogFile::BuildFilePath() {
	GetLocalTime(&m_tSystemTime);
	int iReturn = _sntprintf(m_szFileNameFullPath, MAX_PATH, _T("%s\\%s.%s.%04d%02d%02d.%p.txt"),
		m_szPathName, m_szFileName4Host, m_szFileName4User,
		m_tSystemTime.wYear, m_tSystemTime.wMonth, m_tSystemTime.wDay, this);

	return 0 < iReturn;
}

bool CLogFile::BuildPathAndFilePath() {
	bool bReturn = 0 < _sntprintf(m_szPathName, MAX_PATH, _T("%s\\%s"), m_szPathName4Host, m_szPathName4User);
	if (bReturn) {
		bReturn = BuildFilePath();
	}
	return bReturn;
}

bool CLogFile::Record(LPCTSTR pszFormat, ...) {
	int iReturn = 0;
	FILE* pFile = NULL;
	do {
		if (!IsPathOrFileExist(m_szPathName)) {
			break;
		}
		if (!BuildFilePath()) {
			break;
		}
		bool bIsNew = !IsPathOrFileExist(m_szFileNameFullPath);
		pFile = _tfopen(m_szFileNameFullPath, _T("a"));
		if (!pFile) {
			break;
		}
		if (bIsNew) {
			iReturn = _ftprintf(pFile, _T("Time\tUser\t%s\n"), m_szHeader);
			if (0 >= iReturn) {
				break;
			}
		}
		va_list vlArgs;
		va_start(vlArgs, pszFormat);
		iReturn = _vsntprintf(m_szLine, MAX_LENGTH_CONTENT_PER_LINE, pszFormat, vlArgs);
		va_end(vlArgs);
		if (0 > iReturn) {
			break;
		}
		iReturn = _ftprintf(pFile, _T("%02d:%02d:%02d.%03d\t%s\t%s\n"), m_tSystemTime.wHour,
			m_tSystemTime.wMinute, m_tSystemTime.wSecond, m_tSystemTime.wMilliseconds,
			m_szFileName4User, m_szLine);
	} while (false);
	if (pFile) {
		fclose(pFile);
	}
	return 0 < iReturn;
}

bool CLogFile::IsPathOrFileExist(LPCTSTR pszPathOrFileName) {
	return (0 == _taccess(pszPathOrFileName, 0));
}

bool CLogFile::SetPathName4Host(LPCTSTR pszPathName4Host) {
	_tcsncpy(m_szPathName4Host, pszPathName4Host, MAX_PATH);
	return BuildPathAndFilePath();
}

bool CLogFile::SetPathName4User(LPCTSTR pszPathName4User) {
	_tcsncpy(m_szPathName4User, pszPathName4User, MAX_PATH);
	return BuildPathAndFilePath();
}

bool CLogFile::SetFileName4Host(LPCTSTR pszFileName4Host) {
	_tcsncpy(m_szFileName4Host, pszFileName4Host, MAX_PATH);
	return BuildFilePath();
}

bool CLogFile::SetFileName4User(LPCTSTR szFileName4User) {
	_tcsncpy(m_szFileName4User, szFileName4User, MAX_PATH);
	return BuildFilePath();
}

bool CLogFile::SetHeader(LPCTSTR szHeader) {
	_tcsncpy(m_szHeader, szHeader, MAX_PATH);
	return true;
}

LPCTSTR CLogFile::GetPathName4Host() {
	return m_szPathName4Host;
}

LPCTSTR CLogFile::GetPathName4User() {
	return m_szPathName4User;
}

LPCTSTR CLogFile::GetFileName4Host() {
	return m_szFileName4Host;
}

LPCTSTR CLogFile::GetFileName4User() {
	return m_szFileName4User;
}

LPCTSTR CLogFile::GetPathName() {
	return m_szPathName;
}

LPCTSTR CLogFile::GetFileNameFullPath() {
	return m_szFileNameFullPath;
}

        代码的第一眼感觉,没有注释!这帮高手果然很清高啊,他们不会帮助你看懂,因为这个世界上总有人不配看懂。只能沉下心来,自力更生了。再仔细看代码,会发现代码中的变量名、方法名都很长。第一个战术级思想浮出水面:全名命名规则。命名使用单词全名,而很多程序员喜欢使用缩写,而缩写并不一定能与所有人达成共识,导致命名的意义大打折扣。良好的命名可以代替注释,且效率更高。微软的函数命名平均长度是13个字母,而0day代码中的命名平均长度是16.8个字母,超过微软水准将近30%。可以说,命名平均长度能够作为代码段位的参考之一。

        再仔细看每一个命名,第二个战术级思想浮出水面:前缀命名规则。所有的变量名都有类型前缀,如字符串变量的前缀是“sz”,字符串指针变量的前缀是“psz ,整型(int)变量的前缀是“i ,布尔型(bool)变量的前缀是“b”,这些类型前缀虽然使用了缩写,但这些缩写都是C/C++程序员所共识的。还有,本地变量都没有特别前缀,而所有的类成员级变量在类型前缀前再加上“m_”前缀指示作用域,mmember的缩写,其实还有一个“g_”前缀代表全局作用域,gglobal的缩写,但全局变量只有在C代码中很常见,而在C++代码中几乎从不使用。这些前缀可以让程序员无需向前翻看变量定义,就能知道变量的类型和作用域,节省时间。

        再再仔细看每一个命名,第三个战术级思想浮出水面:名词前置命名规则。例如类成员字符串变量“宿主路径名”命名为m_szPathName4Host,“用户路径名”命名为m_szPathName4User,如果按人类正常思维应该是m_szHostPathNamem_szUserPathName才对,但他们却名词前置,形容词动词后置。其目的是为了给相关命名进行分类,最早是为了能在代码统计工具的报告中,能把相关命名在α排序中排在一起,以便进行代码分析;而在后来的现代IDE的代码编辑器中,都有自动完成功能,根据输入的部分字母自动提示可能的输入,按名词前置命名规则,提示内容将把相关命名排在一起,便于程序员选择。如键入“m_szP”,将提示出m_szPathName4Hostm_szPathName4User,方便程序员在使用相关变量或方法时提高效率。

        在上面提到的命名中,都有一个阿拉伯数字4,这是什么鬼?第四个战术级思想浮出水面:介词缩写命名规则。用4的英文谐音代替介词“for”,原命名应为m_szPathNameForHost,介词作为前置命名分类与后置形容词、动词的分界线被大量使用,为节约键击次数而在组织内约定的缩写。类似还有2,谐音英文的“to”,因为在程序中各种转换也非常多,如BinToHex(二进制转十六进制),可以缩写为Bin2Hex。这可以理解为长命名思想与少键击思想的辩证统一。

          看上面的成员变量和对应的设置方法名和获取方法名,第五个战术级思想浮出水面:对称命名规则。如此整齐划一的命名,不但能帮助阅读者在没有注释的情况下快速理解各方法的意图,还能让使用者无需翻看源码就能准确调用。

        一个小小的命名,是杀机四伏,下足了功夫。就是因为命名是代码的基石,它是多米诺骨牌效应里的第一块骨牌,每块砖不做好,将会影响整个大厦的安危。这些命名规则的终极目标都是为了用空间换时间。在你的每一次键击中,每个思想可能只为你节约了0.1秒,但经不住长年累月的积累,你的有效编程时间就是比别人多,还没开始比赛,你就已经胜过了。伟大往往都藏在细节中。

        代码中还有一个不易察觉的细节,其代码缩进使用的是制表符(TAB键),第六个战术级思想浮出水面:使用制表符缩进。关于缩进使用制表符还是空格,业界一直争论不断,且没什么定论,主要原因就是觉得这是个小问题,无伤大雅,大家随意,开心就好。但这些顶尖高手只用制表符,原因很暖心,仅仅是为了尊重同行!制表符最早出现是为了控制打印机在打印时的左边距,当时定义为8个空格,可视化编程出现后才用于代码缩进,但当时显示器的分辨率是320*200,一行最多显示80个字符,这8个空格实在是太长了,于是就在编辑器中定义为4个空格,但后来有人觉得2个才好,还有人觉得1个更好,最后干脆作为编辑器配置项,根据喜好自定义吧。所以使用制表符缩进的代码在编辑器中的显示样式将会符合当前使用者的习惯,而使用空格缩进的代码将可能会导致当前使用者不适。多么细致的人文关怀,面向人性编程,面向开发者编程,时刻谨记。

        观察代码中的每一个方法,发现都有返回值,哪怕是返回固定值!

        第七个战术级思想浮出水面:调用必须有返回值。绝大多数编程语言都允许调用没有返回值,但这帮顶级精英为什么在可以用这个规则的情况下还是不用呢?这就是接口的艺术,为了向下兼容,未雨绸缪,面向未来编程!因为谁也无法预测,随着代码的不断迭代,这个方法的使用条件可能会发生变化,而有返回值的调用是可以兼容没有返回值的调用的,这样可保持接口的历史一致性,进退自如。这样的设计一旦在public调用中发挥过一次作用,可就不是节约0.1秒的事了。

        上面是Record方法中的一段代码,使用了一个do-while循环语句,但循环条件是个固定布尔值false,意味着这个循环永远只会执行一次,但为什么还要用循环语句呢?如果不用循环语句,正常的写法应该是这样的:

        第八个战术级思想浮出水面:减少嵌套深度。嵌套深度决定了人类大脑的思考深度,而思考深度则决定了消耗的能量和思考的难度。所以嵌套深度较低的代码,让人思考起来会比较轻松且不易出错,而重度嵌套的代码则更容易让人疲倦且增加产生bug的几率。

        这段代码使用do-while循环语句的结构,并配合break语句来减少逻辑嵌套。第九个战术级思想浮出水面:辩证使用gotobreak语句的本质是goto语句,只是受限而已。而goto语句在早期面向过程编程的时代,由于其高效的操作效率而被滥用,把代码写的像面条一样,扯不清,理还乱,这导致了上世纪60年代的软件危机,并最终引发了软件工程革命。在面向对象编程的时代,业界统一的共识是禁止使用goto。但goto语句的操作效率确实很高,所以善用break这种阉割版goto可以起到鱼与熊掌兼得的效果。

        观察Record方法的代码行数达到36行,但业界一般的说法是每个函数的代码行数不要超过30行,理由是人类的脑容量问题。但0day的判断标准是,第十个战术级思想浮出水面:同一函数代码不要跨屏。只要任意函数的所有代码在当前流行屏幕尺寸大小下能够完全显示即可。理由是只要整个代码逻辑在人的静态目视范围之内,程序员的脑容量都够用。除了靠减少代码行数来防止纵向滚动屏幕,前面说的减少逻辑嵌套还能防止横向滚动屏幕。代码逻辑禁止跨屏规则能在很大程度上降低bug产生的几率。

        再观察Record方法,发现一段有趣的代码:

         BuildFilePath方法用于构造日志文件名,其中使用了系统日期作为文件名的一部分,目的就是把日志文件按天分隔,以防止文件过大。这意味着每次写日志,都应判断是否该更换文件名,但这种更换每天只发生一次。而这段代码并没有根据日期是否更改而构造文件名,而是每次都按当前日期构造文件名,这意味着文件名在一天内的绝大部分调用中都是重复构造相同的文件名,这不是做无用功吗?第十一个战术级思想浮出水面:尽量使用顺序代码结构代替判断代码结构。判断语句是bug产生的源泉,尽量不要使用,哪怕代码看上去有点愚蠢。不认同的人可以试试,使用判断语句来修改这段代码,让其看起来似乎更有效率。当你被源源不断的bug改到怀疑人生时,你才能真切地体会到这个以时间换空间思想的精妙之处。对这个未知世界,心存敬畏,才能保你福如东海,寿比南山。

        再观察Record方法的定义,使用了非常罕见的不定长参数,第一个战略级思想横空出世:赠人玫瑰,手有余香。作为一个日志文件类的主要方法,通常就是把传入的字符串参数,存储到日志文件里就好了。为什么要使用一个非常冷门的技术?原因就是尊重传统,方便你的同行,让调用者更干、更爽、更安心。如果参数是一个字符串,则意味着调用方必须在调用此方法前,拼装好字符串:

        使用不定长参数,则可以这样调用:

        一行搞定!把方便留给别人,把困难留给自己,雷锋精神时刻谨记。面向人性编程,面向开发者编程,面向开源编程。

        通览整体代码,系统调用只使用过一次Windows API,其余均使用C运行时库函数,第二个战略级思想横空出世:简单通用。作为一个工具类,会被广泛使用,包括跨平台应用。如果使用Windows API,此类要移植到Unix/Linux平台上将付出巨大代价。而C运行时库函数是语言标准而非平台标准,在功能表现上所有平台都是一致的,所以移植成本要低的多。而且代码中还使用了C运行时库函数的自适应字符集宏定义版本,使得此工具类无论编译目标应用是MBCS字符集还是Unicode字符集都无需修改一行代码!事实上,此工具类在组织内不但有多平台版本,甚至还有多语言版本,包括C#javaVB等。受益于使用语言标准的设计思想,各语言、平台版本的代码一致性很高,产生bug的几率很小,移植成本非常低。

        现在我们正式开始通过浏览代码来理解代码逻辑,先看类构造函数:

        这个初始化还是相当复杂的,对关键类成员变量的默认值进行了规划和设计,第三个战略级思想横空出世:默认值的艺术。为便于理解程序的设计思想,我写了一个测试程序,使用不同的参数调用构造方法,然后调用对应的成员变量获取方法,以查看成员变量的内容:

        从以上结果可以看出,构造函数通过传递不同的参数,将成员变量初始化为不同使用理念的数据套。目的就是让调用者在构造完类后,即可使用Record方法开始记录日志,而无需任何配置!还是那句老话:把方便留给别人,把麻烦留给自己,雷锋精神时刻谨记。面向人性编程,面向开发者编程,面向开源编程。

        继续仔细研读测试结果,可了解代码初始化意图:给构造函数传参空指针(NULL),则日志文件路径自动配置为当前可执行文件路径,紧接着调用Record方法即可产生日志文件,nice!如果给构造函数传参非空字符串,如示例中是“log”,则自动配置日志文件路径为当前可执行文件路径后再附加“log”路径,enn…如果传参是空字符串或不传任何参数(这是默认情况,应该是该类建议的主要使用方式),则自动配置日志文件路径为当前可执行文件路径后再附加带前缀“.”的不包括扩展名的可执行文件名,what

        代码是看懂了,但为啥?难道要自动创建如此诡异的路径?但在Record方法中,不但没有找到创建路径的方法,还看到了这样一段代码:

        这段代码的意思就是当日志文件路径不存在时,将退出Record功能,什么也不干!这个类的作用不就是记录日志吗?居然在某些情况下还不应记录?What the fuck

        第一颗核弹君临天下:即时热调试。像C/C++这种接近硬件底层的编译型语言,预定义有两种编译应用的形态:debug版本和release版本。debug版本用于在开发环境中调试,尤其是单步调试功能可以解决硬核的技术问题,而release版本用于正式发布,没有调试功能。但代码调试时,除了技术问题,还有更大量的业务逻辑问题需要调试。如果使用单步调试效率太低了,所以绝大多数C/C++程序员在debug版本中通过输出日志调试业务逻辑。这些日志通过宏定义控制只在debug版本中编译,而在release版本中忽略,因为正式发布的软件不能在用户方产生大量调试日志,否则日积月累会塞满用户的存储空间。但是,谁也不能保证在debug版本中能调试完所有的业务逻辑问题,如果在用户方部署的release版本出错,大家束手无策。在互联网发明以前,这个问题到也不太重要,因为即使在用户方发现程序错误,程序员也没办法到达现场解决问题。但现在的互联网技术可以支撑远程登录服务器或者个人计算机,赋予了技术支持人员可以在任何时间、任何地点、使用任何设备到达错误现场的能力,但老旧的编译时debugrelease机制,在新时代下也没什么卵用。0day的精英们与时俱进,设计了这个动态debugrelease机制:如果在当前可执行文件的目录下,存在一个特别指定的目录,则程序进入debug状态,并在那个目录下生成日志;否则程序保持release状态,不输出日志。牛B的思想闪耀星空!把代码的debugrelease状态确认由编译时后移到运行时,这意味着当程序发生业务逻辑问题,程序员可直接登录到现场,程序都不用重启,直接建立指定目录,即可知道当前程序正在干什么,找到问题后,再把目录一删,挥一挥衣袖,不带走一片云彩!

        这个顶级设计还有一些非常贴心的细节设计,第一是关于那个指定的目录。默认是不包括扩展名的当前可执行文件名,前面还有一个“.”。这是为了保持跨平台操作的一致性,因为Unix/Linux平台下的可执行文件没有扩展名,如果单纯使用当前可执行文件名,则因为重名而无法创建目录,所以前面加个“.”来保证不重名,还顺便成为隐藏目录,因为Unix/Linux的文件系统定义以“.”开头的目录或文件具备隐藏属性。虽然Windows平台下不存在这些问题,但0day的绝大多数精英都是Windows平台和Unix/Linux平台双料王牌,经常需要在多平台间切换工作,为保持操作一致性,只好委屈一下Windows平台了。但也无需焦虑,这个指定目录可以在初始化或运行时随便修改。修改指定目录还有一个使用技巧,比如在同一目录下有A1A2A3B1B25个应用程序,其中A1A2A3是有钩稽关系的第一组应用,B1B2是有钩稽关系的第二组应用,可以设计为建立A目录,则在A目录中同时产生A1A2A3的日志,建立B目录,则在B目录中同时产生B1B2的日志,达到相关应用群日志按需自动分组的目的。

        第二是关于日志文件名。整个文件名分为5个部分:第一部分是应用程序名,这个很容易理解,一看就知道这个日志是哪个应用产生的;第二部分是一个自定义的名字,这个作用比较硬核,咱门后面再讲;第三部分是日志产生的日期,为了防止文件过大,每个应用程序每天只有一个日志文件;第四部分比较特殊,是运行时日志文件类实例的内存地址,what?这能干啥用?使用实例的内存地址意味着每次启动这个类,文件名就会发生变化,可用于指示这个应用程序在这个日期下的不同启动批次。第五部分是固定扩展名“txt”,指示系统可用文本编辑器打开此文件。整个设计考虑了使用上的方方面面,主要是排序尽量让使用者更方便、更舒适,爱心妈妈,呵护全家。面向人性编程,面向开发者编程,面向开源编程。

        再看向文件写入内容的代码中使用制表符“\t”作为输出内容的分隔符,为什么不是空格?第二颗核弹石破天惊:统计日志。为了能理解这个设计,咱们先看看调用方是如何使用这个类的,典型调用像这样:

        意图就是把需要输出的状态、数据,如调用的方法名、错误描述等,组织成类似表格字段的方式分隔输出。使用制表符可以保证用表格软件打开日志文件或把文本复制到表格软件里,效果是这样的:

        在第一个核弹的淫威下,再加上日志的记录时间精确到毫秒的加持,程序员们彻底放开了,写日志跟不要钱似的,疯狂输出,几乎每个函数在返回前都会把当前处理结果输出到日志里,面对这样的海量日志,用眼睛找bug会看瞎的。所以创造性的利用表格的相关排序、分类汇总、透视图等统计功能,快准狠地定位查找目标。比如示例那个日志,用透视图看是这样的:

         或者是这样的:

        就说你想查啥?咋样都行,就是拖拖拽拽的事。bug往哪里躲?它太难了…羽扇纶巾,谈笑间,强撸灰飞烟灭。

        还记得前面讲到文件名的第二部分吗?就是代码里的成员变量m_szFileName4User,它是干什么用的?看遍代码的上上下下,也看不出个所以然。我们对其赋值“robot”,看看出现啥情况:

        日志文件名第二部分变成“robot”,日志文件中user列里面填充“robot”,仍然一头雾水!第三颗潜射核弹韬迹隐智:调试多线程。多线程调试是程序员的噩梦,因为人类的大脑无法精确模拟计算机多线程的运行过程。所以多线程程序所产生的bug,尤其是无法必现的bug,常常让人束手无策。在前面两颗核弹的加持下,给解决这个问题带来了希望。但如果在日志文件类中加入同步机制,多个线程共享同一个日志文件类实例,则会导致多线程程序在调试状态下被强制串行化为单线程程序,由于运行环境的变化很可能触发不了那个多线程bug,所以每个线程必须单独使用各自的日志文件实例。在分析时,将相关的所有线程日志全部拷贝到一个表格文件中,然后对时间列排序就能知道每个时刻,各个线程都正在干什么。这个user列就是用于区分这条记录来自于哪个线程。多线程调试就这么被轻松地搞定了!说了那么多伟大,我都厌倦了:“老婆,快出来看上帝。”

        我已把这段代码上传到gitee,访问地址CTools: 这是一个工具集,欢迎大家共建,也可应用于项目中,给大家在代码江湖中探险时提供一把趁手的兵器。

        最后,希望同学们也能够创造出有思想,有灵魂,举手投足之间都透露出优雅的代码:

吕秀菱.黑暗天使(LIQUID SKIES,THE GOLDEN SUTRA)

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值