`
hzbook
  • 浏览: 259057 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

 
阅读更多

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

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

软件系统的复杂度是不可避免的。有些问题就是很难,它们的解决方案很复杂。然而,你在软件中找到的大多数复杂度是我们自己造成的。在《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++代码:

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

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

Time.cpp
class Timer
{
public:
    Timer(const time_t sec, const suseconds_t usec) {
        _interval.tv_sec = sec;
        _interval.tv_usec = usec;
        gettimeofday(&_start, 0);
    }
    
    bool triggered() {
        struct timeval now;
        struct timeval elapsed;
        gettimeofday(&now, 0);
        timersub(&now, &_start, &elapsed);
        return timercmp(&elapsed, &_interval, >);
    }
    
    void reset() {
        gettimeofday(&_start, 0);
    }

private:
    struct timeval _interval;
    struct timeval _start;
};
我们现在可以简化循环了:

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

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


分享到:
评论

相关推荐

    程序员必知的硬核知识大全.pdf

    "程序员必知的硬核知识大全" 本资源摘要信息涵盖了程序员需要了解的硬核知识大全,涉及到计算机硬件、操作系统、编程语言等多个方面。 一、计算机硬件知识 * CPU(Central Processing Unit):程序员需要了解 CPU...

    程序员必知excel操作

    下面将详细介绍一些程序员必知的Excel操作,帮助提升工作效率。 1. 数据整理: - **排序与筛选**:对数据进行升序或降序排列,利用条件筛选找出特定数据。 - **合并单元格**:在报表头部创建合并的标题,注意合并...

    程序员必知必会经典算法

    "程序员必知必会经典算法"这个主题涵盖了编程领域中的重要概念,包括基础算法和数据结构,这些都是C、C++等语言中不可或缺的部分。下面将详细讨论这些经典算法及其在实际编程中的应用。 首先,我们要理解什么是算法...

    PHP程序员必知安全法则《高手必看版》

    ### PHP程序员必知的安全法则详解 #### 规则1:绝不信任外部数据或输入 在Web应用程序的安全性中,首要原则就是对所有外部数据持怀疑态度。外部数据指的是任何非直接由程序员在PHP代码中输入的数据,这包括但不...

    《高质量C之高级程序员必知》

    《高质量C之高级程序员必知》是一本专为有志于提升C语言编程技能的开发者设计的指南。这本书深入探讨了C语言的核心概念、最佳实践以及高级编程技巧,旨在帮助程序员从初阶晋升到高阶水平。以下是该主题中涵盖的一些...

    程序员代码面试指南:IT名企算法与数据结构题目最优解-代码

    《程序员代码面试指南:IT名企算法与数据结构题目最优解-代码》是一部专为准备IT企业面试的程序员量身定制的指南。本书的核心内容围绕算法和数据结构展开,通过Java语言实现,旨在帮助读者掌握解决常见面试问题的...

    好的程序员如何写代码

    以下是一些关于好的程序员写代码的特点和技巧: 1. **整洁规范**:好的程序员注重代码的视觉美感,空白和注释的布局都显得错落有致,遵循统一的命名和排版规范,使得代码易于阅读和理解。 2. **命名一致性**:他们...

    程序员的代码的代码的代码

    "程序员的代码的代码的代码"这个标题可能是在强调代码的层次性、嵌套性或者递归性,暗示着代码间的相互依赖和复杂性。在描述中,连续重复的短语进一步突出了这一点,可能意味着代码的深度集成或者高度复杂性。 首先...

    《代码大全》——程序员必看系列

    书中涵盖了从代码组织、命名规范到错误处理、调试技巧等多个方面,旨在帮助程序员写出高质量的代码,提高软件的可读性、可扩展性和可维护性。 “基础”标签表明了这本书对编程基础知识的重视。书中可能包括变量、...

    程序员必知的硬核知识大全.zip

    《程序员必知的硬核知识大全》是一份涵盖...总结来说,《程序员必知的硬核知识大全》这份资料全面覆盖了程序员需要掌握的各种核心技术,无论是初学者还是经验丰富的开发者,都能从中受益匪浅,不断提升自己的专业水平。

    程序员数学3 的源代码

    《程序员数学3》是一本专为程序员设计的数学学习书籍,其主要目的是帮助读者理解并应用数学在实际编程中的重要性。这本书强调易懂性,通过简洁明了的讲解方式,使得复杂的数学概念变得平易近人。尤其适合那些希望...

    js实现3D烟花特效,视觉盛宴,自带爆炸音效,浪漫表白代码,程序员必看!

    js实现3D烟花特效,视觉盛宴,自带爆炸音效,浪漫表白代码,程序员必看! js实现3D烟花特效,视觉盛宴,自带爆炸音效,浪漫表白代码,程序员必看! js实现3D烟花特效,视觉盛宴,自带爆炸音效,浪漫表白代码,程序员...

    程序员必知的硬核知识大全

    程序员在职业生涯中,掌握一些硬核知识是必不可少的。这些知识涵盖了从计算机底层到上层应用的多个领域,包括但不限于CPU、内存管理、进制转换、操作系统原理、BIOS与引导过程、汇编语言、软件与硬件交互等。下面将...

    程序员代码面试指南 IT名企算法与数据结构题目最优解.zip

    《程序员代码面试指南》是一本针对IT行业求职者,尤其是准备进入知名企业的程序员们的重要参考资料。这本书主要聚焦于算法和数据结构,旨在帮助读者掌握在面试中常见的问题,并提供最优解。"左神"作为标签,暗示了这...

    程序员代码面试指南-代码

    程序员代码面试指南:IT名企算法与数据结构题目最优解左程云 著 这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现。针对当前程序员面试缺乏权威题目汇总这一痛点,...

    程序员必学软件程序员必学软件

    "程序员必学软件"这个主题涵盖了从开发环境、版本控制、代码编辑器到调试工具等各个方面。以下是一些程序员必须了解并熟练使用的软件: 1. 开发环境:开发环境是编写和运行代码的基础,例如Visual Studio、Eclipse...

    java程序员必知的十种程序算法.doc

    java程序员必知的十种程序算法.doc

    程序员必看书籍

    7. **数据库技术**:《数据库系统概念》、《MySQL必知必会》等书籍,帮助开发者掌握数据存储和查询的技巧。 8. **前端开发**:对于Web开发者,《JavaScript高级程序设计》、《CSS权威指南》等书籍不容错过。 9. **...

Global site tag (gtag.js) - Google Analytics