专业程序员必知必会的技巧:驯服复杂代码

你从入职第一天起就要应对复杂代码。

若是还未遇到过无法理解的程序,那说明你编程的年头还不够长。在行业里,要不了多久你就会碰到让人发懵的混乱代码:巨兽、面条工厂、来自地狱的遗留系统。我曾接手过一个程序,它的前任在听说要增加一个分量不轻的新特性时,选择了辞职。(我并不怪他。)

软件系统的复杂度是不可避免的。有些问题就是很难,它们的解决方案很复杂。然而,你在软件中找到的大多数复杂度是我们自己造成的。在《The Mythical Man-Month》(人月神话)[Bro95]里,Fred Brooks将复杂度的两个来源分成必然(necessary)复杂度和偶然(accidental)复杂度。

这里有一种区分必然复杂度和偶然复杂度的思考方法:什么复杂度是问题域固有的?假设你面对的是一个日期/时间处理代码散落各处的程序。在处理时间时,存在一些必然复杂度:每月的天数不同,必须考虑闰年,等等。但多数我碰到的程序充斥着大量与处理时间相关的偶然复杂度:用不同格式保存的时间,加减时间的新奇(同时也是充满Bug的)方法,不一致的时间打印格式,说都说不完。

复杂度的死亡螺线

编程时常会遇到这种情况:产品代码库中的偶然复杂度渐渐压倒必然复杂度。情况在某一时刻会自我放大,我称这种现象为复杂度的死亡螺线,如图1所示。


图1 复杂度的死亡螺线

问题1:代码规模

构建产品时,它的代码规模最终将远超任何在学校或消遣项目中所遇到的。行业中的代码库的度量结果从成千到上百万代码行(Line of Code, LOC)不等。

John Lions在《Lions’ Commentary on UNIX 6th Edition》一书中写道:单个程序员能够理解和维护的程序大小的实际限制规模是1万行代码。于1975年发布的UNIX第6版的规模大约是9000行代码(不算机器特定的设备驱动程序)。

相比而言,Windows NT在1993年有4百万~5百万行代码。10年后,Windows Server 2003配备了2000名开发人员和2000名测试人员,他们管理多达5千万行代码。大多数行业项目并不像Windows那样巨大,但它们也都轻易地跨过了Lions设定的1万行代码的警戒线。这样的规模意味着公司内部没有人能理解整个代码库。

问题2:复杂度

随着代码规模的增长,最初想法的概念优雅性消失了。曾经对于车库中两个小伙水晶般清澈的想法变成了大批开发人员艰难跋涉其中的阴暗沼泽。

复杂度并不是代码规模的必然产物。大型代码库完全有可能被拆分成许多模块,其中每个模块都有清晰的用途、优雅的实现和为人熟知的与邻近模块的交互。

然而,即使设计良好的系统也会在它们变大时变得复杂。一旦没有一个人可以理解整个系统,这时多个人必须去理解系统中自己那部分—且没有人的理解跟其他人是完全一样的。

问题3:Bug

产品复杂度飙升,Bug也就不可避免地出现了。这是注定的—就算是伟大的程序员也不是完人。但每个Bug并非生而平等:高度复杂系统里的那些Bug尤其难觅踪迹。总是听到程序员说:“真搞不懂,伙计,系统刚刚崩溃了。”欢迎来到这糟糕的调试世界!

问题4:快速修补

问题并不在于产品是否有Bug—它肯定有,关键在于工程团队在出现Bug之后如何响应。在推出产品的压力之下,大多数程序员经常求助于快速修补。

快速修补是给问题打补丁,而非解决其根本原因。甚至常常不寻找根本原因。

程序员:在试图往网络队列中放入一个任务(job)且队列在10秒内无响应时,程序崩溃了。

经理:重试队列操作100次。

根本原因是什么?天知道,只要重试次数够多,你就可以掩盖任何问题。但如车身修补一样,某一位置的霸道胶水(Bondo)比实际残留的车本身部件还要多。

更难找的问题发生在补丁并没有解决问题根本原因的时候,问题通常根本没有消失—它只是转移到别处。在前面的对话中,重试100次可能很好地掩盖了问题,但万一需要101次重试怎么办?经理只是随便捏了个数字,这种膏药式修补只会让问题更难查。

沿着“快速修补”上行,我们现在得到了一个增加代码规模的完整闭环。

走向清晰

提起复杂的反面,人们通常会想到简单。但由于领域的必然复杂度,我们并不是总能写出简单的代码。应对复杂更好的方法是清晰。你是不是明白自己的代码要做什么?

明确两点会有助于我们减少软件偶然复杂度:清晰思考和清晰表达。

清晰思考

在分析问题的原因时,我们试图做出像“保存时间的方式应该只有一种”这样的清晰陈述。那为何UNIX C代码里还混杂着像time_t、struct timeval和struct timespec这样的结构呢?那并不是太清晰。

如何调和你的清晰陈述和UNIX计时功能的复杂度?你需要隔离复杂度,或将其抽象到单个模块中。在C里,这可能是结构体和操作它的函数;在C++里,它会是一个类。模块化设计让程序的其余部分可以用一种清晰的方式推导时间,而不用了解系统计时功能的内部机制。

一旦能将时间作为程序的一个单独模块进行对待,你也就能证明你的计时机制的正确性。完成这一工作的最佳方式就是单独测试,但是同行评审或书写规格说明也行。当一组逻辑是隔离的而不是内嵌在一大段代码体内时,它的测试和严格证明要容易得多。

清晰表达

随着你清晰地思考模块并将它与其余程序隔离,最终程序也就能更清晰地表达它的用途。处理问题域的代码应该真正专注于问题域。

将辅助代码抽出放入自己的模块之后,剩余逻辑读起来应该越来越像问题域的规格说明(虽然有更多分号)。

让我们看看前后对比。我已经无数次看到过如下这种C++代码:

[cpp] view plaincopy
  1. Time.cpp  
  2. void do_stuff_with_progress1()  
  3. {  
  4.     struct timeval start;  
  5.     struct timeval now;  
  6.     gettimeofday(&start, 0);  
  7.     // 干活,每半秒钟打印一条进度消息  
  8.     while (true) {  
  9.         struct timeval elapsed;  
  10.         gettimeofday(&now, 0);  
  11.         timersub(&now, &start, &elapsed);  
  12.         struct timeval interval;  
  13.         interval.tv_sec = 0;  
  14.         interval.tv_usec = 500 * 1000; // 500ms  
  15.         if (timercmp(&elapsed, &interval, >)) {  
  16.             printf("still working on it...\n");  
  17.             start = now;  
  18.         }  
  19.         // 干活……  
  20.     }  
  21. }  
循环的关键是“干活”部分,但在实际干活之前有20行的POSIX计时代码块。这并没有什么不对,但……就没有一种方法让循环保持对其问题域而不是对计时的关注吗?

让我们把所有时间代码放入它自己的类:

[cpp] view plaincopy
  1. Time.cpp  
  2. class Timer  
  3. {  
  4. public:  
  5.     Timer(const time_t sec, const suseconds_t usec) {  
  6.         _interval.tv_sec = sec;  
  7.         _interval.tv_usec = usec;  
  8.         gettimeofday(&_start, 0);  
  9.     }  
  10.       
  11.     bool triggered() {  
  12.         struct timeval now;  
  13.         struct timeval elapsed;  
  14.         gettimeofday(&now, 0);  
  15.         timersub(&now, &_start, &elapsed);  
  16.         return timercmp(&elapsed, &_interval, >);  
  17.     }  
  18.       
  19.     void reset() {  
  20.         gettimeofday(&_start, 0);  
  21.     }  
  22.   
  23. private:  
  24.     struct timeval _interval;  
  25.     struct timeval _start;  
  26. };  
我们现在可以简化循环了:

[cpp] view plaincopy
  1. Time.cpp  
  2. void do_stuff_with_progress2()  
  3. {  
  4.     Timer progress_timer(0, 500 * 1000); // 500ms  
  5.     // 干活,每半秒钟打印一条进度消息  
  6.     while (true) {  
  7.         if (progress_timer.triggered()) {  
  8.             printf("still working on it...\n");  
  9.             progress_timer.reset();  
  10.         }  
  11.         // 干活……  
  12.     }  
  13. }  
计算机在上述两种情况下做的事情是相同的,但考虑第二个例子对程序可维护性带来的影响:

  • Timer类的测试和证明可独立于它在程序中的使用方式。
  • “干活”循环内的计时有了有意义的语义—triggered()和reset(),而不是一堆获取、增加和比较函数。
  • 现在对于计时的终止位置和(捏造的)循环实际起始位置都清晰了。

当工作在巨大丑陋的代码上时,依次考虑:这段代码想表达什么含义?它有没有办法说得更清楚一点?如果它是清晰表达的问题,你需要把那些碍事的代码段抽象出来,同前面展示的Timer类一样。若代码还是有点混乱,那可能是没有清晰思考的产品,需要在设计层面返工。

行动指南

聚焦于可被隔离和严格推导的一个编程方面,如计时。挖掘你正在从事的项目,识别出这样的代码段:若那部分逻辑被抽象到自己的模块,它能否表达得更清晰。

动手尝试更模块化的方法:选一组混乱的代码,分离必然复杂度和偶然复杂度。在这一刻不要操心细节,只看如何可以清晰地表达必要的业务逻辑,假设你有独立模块来处理支撑逻辑。

------------------------------------

本文节选自《程序员修炼之道:专业程序员必知的33个技巧》“技巧4:驯服复杂度”。

书名:《程序员修炼之道:专业程序员必知的33个技巧》

原书名:New Programmer's Survival Manual: Navigate Your Workplace, Cube Farm, or Startup

作者:Josh Carter

译者:胡键

页数:212

定价:49.00元

ISBN:9787111411642

豆瓣收藏:http://book.douban.com/subject/21323647/

PDF下载:http://vdisk.weibo.com/s/paFl8

当当购买:http://product.dangdang.com/main/product.aspx?product_id=23185207

内容简介:

这是每一位致力于成为专业程序员的软件开发新手都应该阅读的一本书。它是资深软件开发专家Josh Carter 20余年编程生涯的心得体会,从程序员成长的视角,系统总结和阐述了专业程序员在专业技能、编程工具、自我管理、团队协作、工作态度以及需要采取的行动等方面应该掌握的33个非常重要且实用的技巧。作者以自己以及身边的同事积累下来的经验、犯过的错误为素材,旨在为新人们引路,让他们在能力修炼的过程中少走弯路!

全书分为四个部分:第一部分(技巧1~14),从编程技能和工具使用两个方面总结了14个技巧,包含如何正确地书写代码、测试驱动设计、管理代码复杂度、改善遗留代码、代码评审、开发环境优化、自动化等;第二部分(技巧15~24),从自我管理和团队协作两个方面总结了10个技巧,包括如何树立自我形象、压力管理、建立良好人脉和高效会议等;第三部分(技巧25~30),介绍了典型高科技公司的组织结构以及你在整个公司中的位置,并且阐述了薪酬分配的问题;第四部分(技巧31~33),介绍了在日常工作中如何持续改善自己的工作和学习状态。

作者简介:

Josh Carter,资深软件设计师,具有超过20年编程行业从业经验。热衷于编程和追逐前沿技术,但同时谨记史蒂夫•乔布斯的箴言“真正的艺术家能让产品面市”。他还涉足工程管理领域,曾经主管大型企业软件开发团队。目前已出版多本关于计算机软件的技术书籍,同时他还在主流计算机杂志的技术专栏发表文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/435437.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LSGO软件技术团队2015~2016学年第五周(0928~1004)总结

一个充满正能量的学习型团队! 简简单单做人,快快乐乐做事! 本周签到情况统计(第五周0928至1004): 团队技术博客账号统计(第五周0928至1004): 实验室工作台使用情况统计&…

香肠派对电脑版_《香肠派对》是不是除了《和平精英》最成功的吃鸡手游:靠恶搞火了?...

我们知道,自从《绝地求生》自国内外火了之后,国内陆续上线了大大小小不少于10款各种类型的吃鸡手游,从最早的《丛林法则》、到瞬间倒下的《荒野行动》,再到现在最火的《和平精英》和后面即将上线的武侠吃鸡《江湖求生》。国内的游…

C/C++程序员必读的十本书(上)

在Gmail TopLanguage Group 中看到一篇文章“C/C程序员必读的十本书(上)”,大家的讨论都很激励,大家都觉得这篇文章写得很棒,我想在Group里的朋友都能在Lookgirl的这篇文章里获益不少,为此斗胆在此转载该文…

LSGO软件技术团队2015~2016学年第六周(1005~1011)总结

LSGO软件技术团队成立于2010年10月,主要从事的应用方向为互联网与移动互联网(UI设计,前端开发,后台开发),地理信息系统;研究方向为数据分析与计算机视觉。成立几年来为学校培养了一批优秀学生&a…

Silverlight反编译系列二常见代码(自动生成属性CompilerGenerated,代码)

在Silverlight有一些编译后自动生成的代码,最常见的是CompilerGeneratedAttribute和DebuggerNonUserCodeAttribute,下面介绍一下这俩种属性 1.CompilerGeneratedAttribute 自动属性 使用自动属性后,你可以不用手工声明一个私有成员变量以及编…

frontcon函数用不了_真香警告!用了XLOOKUP再也回不去VLOOKUP了

作为一名社畜,Excel是必备技能。如果你不会vlookup函数,都不好意思跟别人说你会用Excel。但vlookup也有很大的局限性,比如:首列必须为查找依据列 无法简单的多条件匹配 横向查找等等往往需要进行一些复杂操作的时候我们都需要修改…

LSGO软件技术团队爬山活动

LSGO软件技术团队成立于2010年10月,主要从事的应用方向为互联网与移动互联网(UI设计,前端开发,后台开发),地理信息系统;研究方向为数据分析与计算机视觉。成立几年来为学校培养了一批优秀学生&a…

HDOJ 2526 HDU 2526 浪漫手机 ACM 2526 IN HDU

MiYu原创, 转帖请注明 : 转载自 ______________白白の屋 题目地址:http://acm.hdu.edu.cn/showproblem.php?pid2526题目描述:浪漫手机Time Limit: 3000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 362 Accepted Submissi…

2引擎帮助文档_Simcenter Amesim 16液压部分帮助文档中英文对照(2)

10/49 Hydraulic Component Design Library10/49液压元件设计库The simplest possible check valve consists of a ball which is free to move over a limited displacement.In one extreme position it is fully closed and completely blocks the flow, and in the other ex…

ArcGIS Engine Runtime 10 Setup步骤

首先安装ArcGIS License Manager 10 Setup 其次安装ArcGIS Engine Runtime 10 Setup 再次,拷贝service.txt文件到ArcGIS\License10.0\bin文件夹下,替换原有文件。 运行ArcGIS 许可服务管理器 - 10.0,启动服务。 最后,运行ArcGIS管…

修改Fiddler实用插件JsonViewer

在Web开发中,Fiddler是我们常用的工具,JSON是我们常用的数据格式。本文所要说的JsonViewer就是一款在Fiddler中查看JSON对象的小插件(当然它不仅仅是作为Fiddler的插件,还有独立运行版和Visual Studio的插件)。关于它的…

matlab用regress方法求ln函数_数学篇|高中数学48条秒杀型公式与方法,一定要掌握!...

「 致于学教育 」高中数学48条秒杀型公式1.适用条件:[直线过焦点],必有ecosA(x-1)/(x1),其中A为直线与焦点所在轴夹角,是锐角。x为分离比,必须大于1。注上述公式适合一切圆锥曲线。如果焦点内分(指的是焦点在所截线段上…

LSGO软件技术团队对外技术交流

LSGO软件技术团队成立于2010年10月,主要从事的应用方向为互联网与移动互联网(UI设计,前端开发,后台开发),地理信息系统;研究方向为数据分析与计算机视觉。成立几年来为学校培养了一批优秀学生&a…

大型项目使用Automake/Autoconf完成编译配置

使用过开源C/C项目的同学们都知道,标准的编译过程已经变成了简单的三部曲:configure/make/make install, 使用起来很方便,不像平时自己写代码,要手写一堆复杂的Makefile,而且换个编译环境,Makefile还需要修…

java中数组的返回值是什么类型_Java数组也是一种数据类型

Java 的数组要求所有的数组元素具有相同的数据类型。因此,在一个数组中,数组元素的类型是唯一的,即一个数组里只能存储一种数据类型的数据,而不能存储多种数据类型的数据。因为 Java 语言是面向对象的语言,而类与类之间…

LSGO软件技术团队内部技术交流

LSGO软件技术团队成立于2010年10月,主要从事的应用方向为互联网与移动互联网(UI设计,前端开发,后台开发),地理信息系统;研究方向为数据分析与计算机视觉。成立几年来为学校培养了一批优秀学生&a…

LSGO软件技术团队2015~2016学年第七周(1012~1018)总结

LSGO软件技术团队成立于2010年10月,主要从事的应用方向为互联网与移动互联网(UI设计,前端开发,后台开发),地理信息系统;研究方向为数据分析与计算机视觉。成立几年来为学校培养了一批优秀学生&a…

idea查询类_Spring Security入门(三): 基于自定义数据库查询的认证实战

0 引言在笔者的上一篇文章中Spring Security入门(二):基于内存的认证一文中有提到过Spring Security实现自定义数据库查询需要你实现UserDetailsService接口,并实现loadUserByUsername(String username)抽象方法。我们可以在UserDetailsService接口的实现…

python计算小数点后有几位_python的数字类型

python的数字类型分为三种,分别是整数int、 浮点数float 和 复数complex。数字是由数字字面值或内置函数与运算符的结果来创建的, 不带修饰的整数字面值会生成整数。包含小数点或幂运算符的数字字面值会生成浮点数。在数字字面值末尾加上 j 或 J 会生成虚…

Window服务的创建与删除

Windows服务应用程序是一种需要长期运行的应用程序,它对于服务器环境特别适合。它没有用户界面,并且也不会产生任何可视输出。任何用户消息都会被写进Windows事件日志。计算机启动时,服务会自动开始运行。它们不要用户一定登录才运行&#xf…