1. XenForo 1.5.14 中文版——支持中文搜索!现已发布!查看详情
  2. Xenforo 爱好者讨论群:215909318 XenForo专区

编程中,有哪些好的习惯从一开始就值得坚持?

本帖由 漂亮的石头2022-01-27 发布。版面名称:知乎日报

  1. 漂亮的石头

    漂亮的石头 版主 管理成员

    注册:
    2012-02-10
    帖子:
    486,020
    赞:
    46
    [​IMG] invalid s,我特别喜欢和聪明人交往,因为不用考虑他们的尊严——乔布斯 阅读原文

    1、先设计,后编码

    每个学过软件工程的人都不应该对这句话感到陌生;但真正能理解它、实践它的人并不多。

    甚至于,还有很多人把它理解成“大公司的繁文缛节,属于大公司病的一种”——他们是这样理解的,也是这样做的。

    我就亲眼见过很多这样翻车的案例。

    当我质问其中一些人,为什么初始设计那么大的漏洞就没人看见时,他们理直气壮的答复说“这不就是个过场嘛”“谁能一开始就想到会有这样的问题”——不是,如果没有能力从一开始就预见到问题的话,你凭什么在总设计师这个职位上占住茅坑不拉屎?

    先设计,后编码,它的意义在于,我们首先要从整体上搞明白这个项目可不可行、都有哪些方案、这些方案都有哪些优劣势,以及不同方案可能带来哪些风险、如何应对……

    一场大仗、硬仗,你没个 plan B,那怎么可能啃下来。

    哪怕小打小闹的一个很小的项目,事先规划一下也比信马由缰好无数倍。

    事实上,业界公认,需求 / 设计阶段应该占软件开发总时长的 60%;但这 60%使得后面的 40%没了悬念,最终整个项目反而能更快、更轻松的上线,且上线后极少有棘手 bug。

    当然,也有不少人没那个水平,这 60%的时间完全是空耗。但那是人的问题,也是公司管理的问题。

    打个比方的话,这就好像开车去北京一样:先设计,后编码要求你先看看地图,大概总结一下去北京有几条路线、路上补给容不容易、消费高不高、容不容易被气候影响,等等。

    弄清了大方向、搞明白了优劣势,然后拿语言准确描述出来:西安去北京,都是京昆高速起始,然后有三条路线:京昆高速就能直达,转京港澳高速货车多,转沧榆高速就绕远了……

    心里有了谱,那么当埋头开发时才不会被细节弄花眼:

    你看大家都走右边了,我们是不是也转过去啊?为什么?不知道啊,但大家都这样走,我们不跟着……不好吧?

    ——人家是货车!

    哎哎哎,听说前面车祸了,堵车!我们得绕道!

    绕哪?

    绕洛阳!小马说了,他洛阳人,那边他熟!

    ——醒醒!我们要去北京,你咋不绕海南呢!

    坏了……迷路了……

    不是,去个北京你都能迷路?不就一条道嘛?

    那不是……半道我听一司机说,去北京可以走 XX 环城路转 YY 省道,不收费……

    嗯,知道。然后呢?

    然后去了太原……

    再然后呢?

    逛了青岛……

    那现在呢?

    我们在哈尔滨。雾霾太大,挡风玻璃糊了,这真没法……

    等等,我们在哪?

    哈尔滨。

    ——合着你不绕海南就绕东北啊。

    别笑。实践中,比这更大更惨烈的笑话多了。比如我吐槽过的某个“五百人开发两年多投资五千万又追加五千万,结果预计支持 2000 万用户的项目才放了两万用户进去盘点一次就要六小时”的奇葩项目——没错,一个多亿打水漂了。

    打水漂的原因,是因为这个项目的盘点逻辑复杂度是 O(N^3)!

    了解复杂度是什么的,自然明白为什么我会吐槽“这个项目需要我们出兵全世界,把所有已经造出的电脑全都抢回来;再把电脑工厂都占领了,为我们免费生产 100 年的电脑,这才能……咳咳,2000 万用户就别想了,支持不了的。我说的是,这才能让我们的高管们顺利搪塞到退休!”

    当然,作为初学者,你可能没做过这么大的项目。

    但哪怕是你为女朋友写的“生日提醒程序”,你都应该先设计、后编码。

    先站在设计的高度,这才能一下子看透问题,解决“电脑关机怎么办”“提示时我刚好不在电脑旁怎么办”“提示时我正在 lol 团战,结果它跳出来闹的我团战失败、生了一肚子闷气,结果把什么都忘了,这该怎么办”等等疑难。

    相反,你要兴冲冲直接动手——生日提醒啊?简单!我就不停的读系统时钟,时间到了就弹窗!啊坏了,关机怎么办?我放到自动运行里!

    写完,一试,CPU 占用 100%,一秒内一口气弹了 8 万个窗口,只能拔电源。

    拔完电源,重启,桌面还没显示利索,另外 8 万个提示窗又来了……

    怎么办怎么办?让我写个程序自动关闭它!

    真好。现在你开个记事本也会自动秒关了——为了女朋友,这台机器……已经成了爆米花机!

    没有事先规划,想哪写哪;结果被层出不穷的状况(还美其名曰需求变更)牵着鼻子、面多了加水水多了加面:这就是绝大多数软件开发时间冗长、状况不断、无疾而终的根本原因。

    2、写“明显没有错误”的代码,不要写“没有明显错误”的代码

    这句话实际上是接着上一条的。

    很简单,除非你真的做了设计,否则你的代码当然不会有什么“设计感”——信马由缰淌出来的,怎么可能有“整齐的设计感”。

    你必须先用心设计,找出所有可以到达目标的路径中、最短最快消耗最低的那一条。

    只有先做到这一点,你才能把代码写简单,才能让人一眼看出意图、明显没有缺陷。

    把很简单的程序搞复杂、搞的没人能懂,这很简单——不妨搜一下“C 语言混乱代码大赛”。

    但把很复杂的功能写简单、写的让人一眼看懂、而且很确定“这里明显没有错误”,反而需要极深的功底。

    尤其一项技术 / 一个领域发展成熟之前,第一个把它写成大白话的,哪个都是业界大拿。

    3、学最激进的技术,用最简单的实现

    这句话又承接了上一条。

    学最激进的技术,是因为“最激进的技术”往往关联着“最天才的思路”。

    我们当然应该及时跟进、融会贯通——然后化用到我们自己的项目里。

    但为了展示,“最激进的技术”往往会极尽炫技之能事——要的就是惊爆一地眼球的效果:这居然也能行!这特么也能行!

    当你乍一看云山雾罩,细一看“……这特么也能行”时,你还可能断定“这代码明显没有错误”吗?

    尤其是,激进的技术都是不成熟的。盲目跟进,后果很严重哦。

    举例来说,C++ 的模板就是一项非常先进、非常好用的技术;但它也是非常激进的——激进到哪怕现在都难说完善的程度。

    包括 C++ stl 库——C++ 基本库啊!基石级的存在!——那可都是……奇计迭出。

    比如——尤其是早年——当你希望它帮你为某个容器里的数据时,你可能得到超过 4k 的、满是<><<>><>><><<>>>>>的错误信息;原因仅仅是:排序算法需要随机访问数据,而你给它的容器不支持“随即迭代器”。所以后来才有了 concept。

    因此,我们应该学习最激进的东西;但只用那些能把工程变得更简单的技术。

    注意,我可没说“学最激进的东西,但千万不要用它”或者“学最激进的东西,然后一定要用它”哦。

    我说的是,第一,最激进的东西要了解;第二,只用足够稳定的东西;第三,也是最重要的一点,技术无所谓先进落后,能最简单平易解决问题的,就是最好的技术。

    ——因此,我会在大部分公司视 STL 为洪水猛兽时,自己在工程中实现模板算法,因为只有模板才能最简洁直观的完成我当时面对的问题;但哪怕到了 C++20 年代,该查表时我也会简单直白的敲出查表算法:只要它在解决问题时更简洁、更直白、效率更高。

    4、把可变的东西隔离到配置数据里

    特意指出这点,是因为这是很多“追新”追到“忘本”的人的通病——病到明目张胆的犯蠢。

    这方面的典型就是面向对象大师们——没错,包括你们崇拜的很多面向对象专家 / 大师,那都是明目张胆的蠢

    这些人的特征是,他们特别见不得代码中的 if,一定要用层峦叠嶂的类继承层次把它包装起来,美其名曰“应对变化”。

    如此理解面向对象编程 | 酷 壳 - CoolShell

    恰恰相反。他们这样子不仅无法应对需求变更,反而造成了代码和数据的深度耦合——过去,我们处理文档数据用 word,处理报表数据用 excel,两个软件应对一切;面向对象大师一来,坏了,我写的文档要用自 word 继承的 invalid_word 类;我写的程序类文档要再继承一层,用 invalid_program_doc 类;而读后感类则要 invalid_textreading_doc 类……

    光我一个人都能把你累死,何况知乎上亿答主……

    面向对象编程的弊端是什么?

    这实在太蠢了。

    请记住,不要把配置参数固化在代码里——尤其不能固化在类继承结构里:这特么就是个但凡长了个脚趾头都不应该犯的低级错误!

    相反,如果真的有需求,请使用配置参数来动态改变类的行为——更严苛更复杂的需求,不妨实现成 DSL(领域特定语言 domain-specific language)。

    但无论如何,不要把配置写死到类继承结构里——只有最白痴最脑残的外行才会这么做。

    哦,对了,被他们误解的“开闭原则”说的其实就是,程序代码写出来就不用再动了,这叫对修改封闭;但随便你需要什么样的功能,都可以借助既有的代码完成,这叫“对扩展开放”。

    你看,当你搞出个 DSL 时,是不是整个领域的一切都能借助它来完成?这是不是最彻底的“对扩展开放”?

    同时,用户都被你隔离到 DSL 那一侧了,你的基本库是不是用就行了、再也用不着也不应该修改?这是不是最彻底的“对扩展开放”?

    总之,设计程序,请一定要记住:我们只写固定不变的东西;经常会变的东西,我们只写 hold 住它变化的那一点精华(此所谓 meta 编程),所有可能变化的东西都应该隔离到数据中去。

    只有这样写,程序才会简单,工作才会轻松,才能做到一出手就是 0bug,才能“虽然我只写了一个很小的程序,然而任你需求万般变化,我只一样应付”。

    尤其是,那些复杂的、处理元规则(meta)的逻辑是难以排错、难以更改的,所以更应该“meta 化”——把这些写的简单,把对它们的调用隔离到外层配置、数据乃至 DSL 中去,程序自然好写、好测。

    嗯,你想一想,假如你写一个 C 语言版本的 hello world,要调试它居然要看编译器源代码,这玩意儿你还有能力调试吗?还有能力 debug 吗?

    反之,当你把“元规则”隔离进编译器、只留给外界一个干干净净纯纯粹粹的 C/C++ 语言说明文档、使得他们哪怕写操作系统都无需碰你的编译器时,这玩意儿是不是才好学、好用、好改?

    还是那句话,记住了:对修改封闭不是你拍桌子砸凳子,撒泼打滚骂高层“这段程序是我的,不改!打死也不改!打死也不给你们改!”

    错了。

    对修改封闭是,不用你说,你的上司,你的同事,就会在别人犯错时这样说:“不,这只是你不会用而已。这套库我们用了很多年了,质量非常好,基本不可能出 bug。请检查你自己的程序,不要遇到点捉摸不定的就去怀疑编译器!”“什么新需求?不不不,你要明白,我们这套库足够应付一切需求了,因为我们可以证明它的对外接口以及功能组是完备的。对,它不能改,也不需要改。当然,如果你们面对的场景很普遍,那么我们也可以在库中增加支持——但仅仅是添加,原有的接口是不需要动的”。

    你看,真正做到了“对修改封闭”,表现是“不需要动”“不要去怀疑”“可以增加功能,但没必要触动已有代码”……

    而“把数据的拓扑结构”都通过“设计模式”固定到代码里、尤其是拿类继承 / 多态来取代 if 的那种蠢做法呢,实质上是“把代码组织结构和数据耦合在一起”——面对多变的需求,这种蠢透了的东西不仅不能灵活应变、反而通过额外的、“继承的拓扑结构”这层维度,把自己绑的更死了。

    所以,我才要专门强调一下:请一定要区分开“业务模型”和“业务数据”;甚至于,请特别警惕,不要被用户自己描述的“业务模型”带歪了:具体的某个公司的组织结构也只是数据,他们这些外行哪能给你合理的总体设计!

    你得自己琢磨对方的组织结构和信息流程,思考下“如果业务部不再从属于客户部,而是独立出来、改名为‘服务部’,和客户部并列,那我的程序该怎么办”……

    你看,这才叫“需求变更”——那些半吊子那种“从一开始就没弄明白、搞了两年了才发现一开始弄错了得改”,那叫“需求事故”,可不是什么需求变更。

    5、多考虑意外情况,不要只实现 happy path

    所谓 happy path,就是完全不考虑出错、不考虑数据竞争、不考虑操作提交时条件不满足、假定世界是完美的、按顺序来一切都能解决的这么一个执行流。

    嗯,如你所见,很多人 happy path 都还设计不好呢……

    但,如果你有所追求的话,就别只盯着 happy path。

    请从你的小的、玩具级别的项目开始,训练自己同时考虑 happy path 和异常退出时的执行流——甚至是异常恢复流。

    做大一些的、需要长时间运行的项目,不会处理异常是不可能的。

    当然,happy path 也是必须先找出来的。

    先把 happy path 找出来,再一点点添加——这里可能出现意外,出现意外怎么办……逐渐添加、丰富下去,正确的设计稿就出来了。

    6、在设计时就考虑好异常处理途径和应对方案

    这一条看似和上一条重复了;但实际上是上一条的深化。

    比如知乎上就有人问过:

    应该如何理解 Erlang 的「任其崩溃」思想?

    很多人,一听说要搞“异常处理”就觉得头大如斗——这怎么搞啊?还不是 catch 掉打一条日志继续……

    没想到,日志越打越多;搞着搞着到处都是异常,于是不得不到处 catch 到处吞异常到处打日志——写了 100 行代码,容错代码倒写了五百行;容错代码的容错代码又写了 1000 行;容错代码的容错代码的容错代码又……

    你看,无组织,无纪律,遇到风吹草动都得记日志……

    啊,倒也简单。也就是每个接口附近都长个肿瘤一样的“容错代码层”嘛。

    于是,整个项目代码量暴增,1 万变 10 万 10 万变百万……然后就再也没人敢碰了。

    不要这样。

    异常处理是需要在设计阶段就考虑好的。

    首先,要把异常分为几类。

    比如,用户输入数据本身的错误、网络错误、磁盘数据被破坏、宇宙射线造成位反转,等等。​

    然后,对确定来源的异常应设计正常处理流程——它是正常流程的一部分,是设计之初就应该考虑好的,可不是什么异常。

    比如,用户输入数据过不了检测,那就打回去让用户重填。
    网络连接出错呢,要给用户提示;如果用户提交了数据,那么可以考虑把这份数据先存在本地(注意数据安全),或者提示他等网络好了重做。
    特别的,有些时候,用户的一连串动作整体构成一个“事务”,比如加购物车、下单、付款、送货、确认收货等。这一系列动作,其中的每一个可以做成“原子”的,比如付款要么成功、要么不成功;但整个事务要允许暂停、要记住每一步的状态——然后,无论用户手机重启还是我们服务器宕机,其中的每一步都要记录在案,绝不能有丝毫差错。​

    之后,对未知来路的奇怪错误,不要姑息——见到了,就让程序崩掉。

    这是因为,此时,我们可能是遇到了宇宙射线,也可能被人缓冲区溢出攻击——或者我们自己错误覆写了某个数据结构。
    此时程序的状态是不可控的,它犯任何错误都有可能。
    我们不应该寄希望奇迹发生、程序突然自己就好了——它好不了。甚至,它真突然“表现正常”了,那才可怕呢:说明我们最最不想见到的情况出现了(比如,黑客成功控制了它,抹去了一切异常痕迹……)

    因此,正确的做法就是:让程序立即崩溃!​

    最终,如果我们的程序的确有极高可靠性要求的话,我们需要设计一个机制,及早发现程序崩溃并自动拉起新的实例。

    举例来说,apache 就非常“激进”:默认的,每个 php 解释器实例在接受过 50 次页面请求后,哪怕没有任何异常,apache 都会强行杀死它、重新拉起一个实例来。

    这是因为,很多时候,程序的异常状态需要存在很久、持续扩散、到了某个“病入膏肓”的状态,才能被迟钝的我们感知。
    因此,当安全特别重要时,我们不妨假定“一个响应过 50 次请求的解释器已经出错了、甚至被黑客攻陷了”——立即杀死它并启动新的实例,对于 Linux 这种起一个进程消耗极低(仅需一个 fork 而已)的 OS 来说,几乎没有可观测的额外负担,但却可以最大限度的保障系统的可靠性。

    所以说,不要把“程序崩溃”看作洪水猛兽。我们 hold 得住它。
    不仅如此,及早让出现了位置状况的程序崩溃,我们也能更容易的找到问题根源——趁着犯罪现场尚未被破坏及早立案侦察,这才能确保罪犯(bug)无处可逃。​

    如此反复,最终就是:一出错就马上抛异常崩溃掉的程序,出错的机率越来越低、渐至于怎么折腾都不会崩溃、甚至单实例都能 7x24 小时可靠运行;而使劲容错、绝不崩溃的程序,它几乎每时每刻都在出错、逼得用户不得不“重启下说不定就好了”“这破系统用十分钟就得重启,不然丢数据……不是丢新数据,旧数据都会被破坏……”

    7、有时候要把用户当作“敌人”防范

    经常见到一些新手程序员,写了个新程序。给人一用,崩了……

    ——哎呀,你别乱点!要先这个,再这个,然后这个……

    ——你要乱点,程序还没初始化呢,那肯定崩……

    ——不行不行,必须 ABC 的顺序依次点下来。你要 ABBC 的点,会怎么样?天知道……

    这样是不行的。

    用户不是专业人员;就是专业人员,人家又不知道你的程序的内部逻辑。

    如果你需要保证顺序,请安排逻辑,确保用户点 C 前必须点 B、点了 B 就得点 C。

    如果你要支持回退,那就理清逻辑,把栈管好。

    甚至,我自己遇到的,某个设备设计来就是监督用户(巡道工)、防止他们偷懒的——他们恨不得把机器用坏、省的带个“随身间谍”打自己小报告呢。

    不仅如此,联网的程序还几乎必然会遭遇黑客攻击……

    因此,请就把用户看成“专门来捣乱的”,甚至看成“无孔不入的黑客”——而你,要写一个程序,经受他们的“严刑拷打”却依然可靠运行。

    这听起来很难,做起来……嗯,也没那么容易。

    但只要你愿意往这方面努力,却也没那么高不可攀——绝对不是什么“黑客无可匹敌,我们只能俯首称臣”……没那回事。

    黑客说白了,也就是个“偏安全侧的白盒测试工程师”——你会怕测试工程师吗?

    那你干嘛怕黑客。

    8、复用的诀窍:只做一件事,把一件事做好

    前面关于软件工程的讨论可能吓到你了:妈耶,写一个可以应对需求变更的东西好难!

    其实一点也不难:只做一件事,把一件事做好,这玩意儿就天然是方便复用的。

    这很容易理解。

    玩过积木吧?

    什么样的积木摆什么造型都用得上?

    方块,对吧。

    为什么方块这么容易复用?

    因为它最简单。

    你看,门拱、门柱之类,只能摆屋顶、摆房门,对吧:

    [​IMG]

    而方块呢,摆哪都可以:

    [​IMG]

    软件也一样。

    你往里面添加了越多的“高级功能”“自动化机制”,它就越发的只能为它的设计目标服务了——稍微改一点?重写吧。

    反之,你把功能简化到简无可简、不让它保存什么在状态,而是用户给什么数据它提供什么服务……那么,很自然的,用户拿它来做什么都可以。

    Linux 的 grep、awk 等犀利的文本工具,其实就是这么来的。

    这实际上也是我前面说过的:把可变的东西隔离到数据中,程序只提供一组元规则!

    你看,grep 是万能的。因为它压根不理睬你的文本是什么、从哪里来;它只是提供了一组文本模式匹配工具而已。

    把具体事务相关的东西隔离出去、只写程序处理“共性”……越是这样,你的程序越简洁。

    程序越简洁,就越是可以随意的拼起来、拼出千变万化五彩缤纷的大千世界。

    当然了,还是那句话,把程序写复杂、写的功能单一死板、一点点“需求变更”就得劳民伤财,这很容易;但想要学会抽共性、写简单、把用户相关的东西尽量往外放、甚至最终只体现于配置文件 /DSL,这很难。

    尤其玩到 DSL,那就进入编译器领域了。

    当然,也别为 DSL 而 DSL。一定要找到技术性 - 简洁度曲线的最低点,这才能最高效率最低成本的完成项目——同时保留无与伦比的扩展性。

    9、学习测试理论,遵循规范,利用工具,有章法的编写程序

    前面“藐视”了一把黑客,恐怕不少不学无术者未免要“友邦惊诧”了。

    但实际上,这个并不难,也是有专业的、现成的体系的——而且有现成的课本,拿来学就是了。

    比如,前面我提到“要把用户当敌人”,恐怕很多人就想不通了:不是,你是不知道,用户能有多奇葩……

    还会有人想起这个笑话:

    一个测试工程师走进一家酒吧,要了一杯啤酒;
    一个测试工程师走进一家酒吧,要了一杯咖啡;
    一个测试工程师走进一家酒吧,要了 0.7 杯啤酒;
    一个测试工程师走进一家酒吧,要了 -1 杯啤酒;
    一个测试工程师走进一家酒吧,要了 232 杯啤酒;
    一个测试工程师走进一家酒吧,要了一杯洗脚水;
    一个测试工程师走进一家酒吧,要了一杯蜥蜴;
    一个测试工程师走进一家酒吧,要了一份 asdfQwer@24dg!&*(@;
    一个测试工程师走进一家酒吧,什么也没要;
    一个测试工程师走进一家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来;
    一个测试工程师走进一家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿;
    一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷;
    一个测试工程师走进一家酒吧,要了 NaN 杯 Null;
    一个测试工程师冲进一家酒吧,要了 500T 啤酒咖啡洗脚水野猫狼牙棒奶茶;
    一个测试工程师把酒吧拆了;
    一个测试工程师化装成老板走进一家酒吧,要了 500 杯啤酒并且不付钱;
    一万个测试工程师在酒吧门外呼啸而过;
    一个测试工程师走进一家酒吧,要了一杯啤酒';DROP TABLE 酒吧;

    测试工程师们满意地离开了酒吧。

    然后一名顾客点了一份炒饭,酒吧炸了。​

    嗯,不可否认,的确“总会有我们意想不到的状况”;但绝大多数情况,我们是可以预想到的。

    不仅可以预想到,甚至测试理论还帮我们压缩了负担。

    比如说,一个人机界面(UI),用户的输入有无穷多种,这怎么测?

    简单,划分等价类。

    什么叫等价类?

    就是把数据分为若干类。合法数据程序必须给出正确响应;非法数据程序必须明确拒绝。

    比如,我们这个程序负责收钱,金额上限 100,下限为 0,精确到小数点后两位小数。

    那么,[100, 0]就是合法数据。

    (0, -∞)和(100, +∞)就是非法数据。

    abc 也是非法数据

    我们不需要测试所有这些数字——那不可能做到;但我们可以从每一类里选择几个进行测试。

    因为程序也是针对集合进行判断的,因此,每类数据选择一两个代表就行了,足以涵盖所有情况。

    然后,一般人经常犯错的,刚刚好介于合法和非法之间的数据,这叫“临界值”,比如 -0.1、0、0.1、99.99、100、100.01 都是边界值——注意边界值和合法 / 非法数据并不是同一层面的东西——我们最好也都找出来,测一测。

    不仅如此。

    有些数据,虽然接口上看都属于合法数据;但内部处理呢,可能大于 30 块钱的走转账、小于 30 块钱走快捷支付——你看,其实这里也要分成两个等价类,这才能覆盖所有流程,对吧。

    同样的,这里也会有新的边界值,比如 29.99、30、30.01 之类。

    你看,快刀斩乱麻,是不是一下子就清晰了?

    其实很多很多东西都这样。你自己得知道背后的原理,知道了,自然觉得一切井井有条,轻松应付;但如果不知道……祝你好运。

    10、正确认识测试和开发的关系

    这在过去是老生常谈;但现在嘛……

    由于国内根深蒂固的等级制思想,测试嘛,他们要求比我低,水平没我高,写不来程序才当了测试……

    所以,测试当然是来伺候大爷我的。我写完程序,丢给他,他负责找出所有错误——漏测了,那是他的责任!

    爽吧?

    然而很遗憾,这只是一时爽。长久来说对你是不利的。

    事实上,请把程序员看作“公司任务的承包商”;而测试工程师呢,则是“公司派来的验收员”——他的职责是给你的软件质量下一个评估,并不是替你找 bug 来了。

    早年一些公司的方法论里面,还建议“当发现软件 bug 过多时,不要提交全部 bug,要让程序员自测,然后验证那些未提交的 bug 是否也被修复了,以确保大部分 bug 已被修正”。

    当然,这些年来,有些公司干脆撤销了测试工程师,让程序员自己测——思路上是一脉相承的,只是把“最终软件质量”踢给了用户。

    那么,为什么业界如此看重程序员自测呢?为什么我说“学会测试”对我们程序员自己的长远发展更有利呢?

    很简单,最熟悉软件的就是程序员自己。

    就国内这绝大多数测试工程师看不懂程序的水平,他们能覆盖接口等价类就不错了;至于内部判断 / 执行流程,他们看不懂,也写不出用例、做不了覆盖。

    哪怕他们能做到,写程序这件事也是程序员在干,轮不到他们。

    那么,怎样才能把一个程序写的简洁、好测、可以“明显没有问题”呢?

    你猜你写完程序二郎腿一翘,等测试奴忙活出结果你再和他扯皮几天……你能学会吗?

    事实上,很多人不光程序越写越糟烂、甚至反而学会隐藏错误了!

    然后,代码质量就……

    相反,当你自己对代码质量就有一个很清晰的认识、对于暴露缺陷的理论方法了如指掌时,你还可能写那些“一看就不大对”的代码吗?

    换句话说,测试测出来、没有 bug 的代码,那叫“没有明显的缺陷”;而程序员写出来就没有 bug 的代码,那才叫“明显没有缺陷”

    想要做到“写明显没有错误的代码”,想要走到这个层次,你就得训练自己,让自己对缺陷敏感、能够持续产出“测试友好”的代码,甚至一出手就是成品,就没人能找到 bug。

    不知道往正确方向努力,那就永远不会有进步。

    11、敏捷 / 测试驱动开发是以上的演进而不是替代

    这个又是国内普遍的、不学无术环境下养成的严重错误认知。

    典型的瀑布式开发,高级工程师要从需求到详设全部包揽,只留一些空函数给程序员填(当年日本人就喜欢这么搞)。

    这种模式对高级工程师要求很高,但对写代码的工程师要求很低,会填空即可。缺点是过于僵死,项目周期长;而且容易搞出庞大、累赘的方案。太容易走进“重分解、轻复用”的误区。

    敏捷其实也是瀑布式开发,也需要从需求到总体设计到模块设计走上一遭;但它同时又汲取了“自底向上”模式和“快速原型法”的长处。

    它要求程序员写出可复用的、库函数水平的代码,这样才可以根据需要随意组合(这是自底向上开发模式的长项;但这个开发模式底层接口简洁优美,高层经常一团乱);它也要求程序员做出可复用的设计、做好模块分解,这样需求变更才不会对程序造成过大冲击(这是瀑布模型的风格,高层设计优美简洁,越往下越不能看);最后,它先做基本功能,让客户看到样子,再一点点改(这又是快速原型法的优点,只是原型法写出来的代码是要扔的,而敏捷方法不扔,留下来继续用)。

    换句话说,敏捷的“先写用例后写代码”,实际上是以单元测试代替详细设计文档、以高质量的面向对象的类定义代替模块设计、总体设计文档,从而把写文档的时间直接拿来写代码——然后借助面向对象框架直接导出文档就行了。

    显然,敏捷模式对工程师的要求非常高。因此,最初搞敏捷的那些人甚至提倡“结对编程”,就是让两个工程师用一台电脑,一个写一个看。据说效率和软件质量比起单人分别开发都更好一些。

    但这样太理想化。因为它要求两个工程师水平得差不多,不然就成了一个人写另一个干看,甚至看都看不懂。

    不仅如此,它还要求这两个工程师都得有总体设计能力、可以一步到位的做出优秀的模块分解设计——这个要求一般公司根本做不到:大多数公司里,能做总体设计 / 模块设计的工程师可能只有一个,甚至连一个都没有。

    但没有这个能力、没有足够的质量,程序就会越改越乱、越改越烂,绝不会越改越符合客户需要。也就是敏捷不起来,搞成了行为艺术。

    再后来,就不再搞结对编程了;而是改成“先写测试,后写代码”——说白了,详细设计文档没人想写,写文档的功夫就把代码写出来了;而且经常写完了文档,才发现实际实现时遇到了问题,还得回头改文档……

    先写测试,其实就是定内部功能划分和接口设计。不然没法写测试用例。写完测试再写代码,写出来就能马上跑一下测试,确保接口正确——倘若发现了问题,那就改测试用例(也就是改接口):反正总是要测的,改一下很自然,不多费工。

    换句话说,现在只要求普通程序员有详细设计的能力,那就足够玩敏捷了——至于模块设计、总体设计、需求等等,那当然还是得专人来做。

    相比之前,这显然就实际多了。

    但我们现在大部分人、大部分公司玩的所谓“敏捷”呢?

    简单说就是我也不会总体设计我也不懂详细设计,爱做啥样做啥样,做完了给客户碰运气,碰过去了喝啤酒,碰不过去加班再改——反正客户有花不完的钱!

    阅读原文
     
正在加载...