作者:invalid s
链接:https://www.zhihu.com/question/32255673/answer/2325523343
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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,一定要用层峦叠嶂的类继承层次把它包装起来,美其名曰“应对变化”。
如此理解面向对象编程 | 酷 壳 - CoolShellcoolshell.cn/articles/8745.html

恰恰相反。他们这样子不仅无法应对需求变更,反而造成了代码和数据的深度耦合——过去,我们处理文档数据用word,处理报表数据用excel,两个软件应对一切;面向对象大师一来,坏了,我写的文档要用自word继承的invalid_word类;我写的程序类文档要再继承一层,用invalid_program_doc类;而读后感类则要invalid_textreading_doc类……
光我一个人都能把你累死,何况知乎上亿答主……
面向对象编程的弊端是什么?7610 赞同 · 355 评论回答
这实在太蠢了。
请记住,不要把配置参数固化在代码里——尤其不能固化在类继承结构里:这特么就是个但凡长了个脚趾头都不应该犯的低级错误!

相反,如果真的有需求,请使用配置参数来动态改变类的行为——更严苛更复杂的需求,不妨实现成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 的「任其崩溃」思想?432 赞同 · 46 评论回答

很多人,一听说要搞“异常处理”就觉得头大如斗——这怎么搞啊?还不是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、复用的诀窍:只做一件事,把一件事做好

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

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

这很容易理解。
玩过积木吧?
什么样的积木摆什么造型都用得上?
方块,对吧。
为什么方块这么容易复用?
因为它最简单。

你看,门拱、门柱之类,只能摆屋顶、摆房门,对吧:
image.png
而方块呢,摆哪都可以:
image.png
软件也一样。
你往里面添加了越多的“高级功能”“自动化机制”,它就越发的只能为它的设计目标服务了——稍微改一点?重写吧。

反之,你把功能简化到简无可简、不让它保存什么在状态,而是用户给什么数据它提供什么服务……那么,很自然的,用户拿它来做什么都可以。
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、敏捷/测试驱动开发是以上的演进而不是替代

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

典型的瀑布式开发,高级工程师要从需求到详设全部包揽,只留一些空函数给程序员填(当年日本人就喜欢这么搞)。
这种模式对高级工程师要求很高,但对写代码的工程师要求很低,会填空即可。缺点是过于僵死,项目周期长;而且容易搞出庞大、累赘的方案。太容易走进“重分解、轻复用”的误区。

敏捷其实也是瀑布式开发,也需要从需求到总体设计到模块设计走上一遭;但它同时又汲取了“自底向上”模式和“快速原型法”的长处。
它要求程序员写出可复用的、库函数水平的代码,这样才可以根据需要随意组合(这是自底向上开发模式的长项;但这个开发模式底层接口简洁优美,高层经常一团乱);它也要求程序员做出可复用的设计、做好模块分解,这样需求变更才不会对程序造成过大冲击(这是瀑布模型的风格,高层设计优美简洁,越往下越不能看);最后,它先做基本功能,让客户看到样子,再一点点改(这又是快速原型法的优点,只是原型法写出来的代码是要扔的,而敏捷方法不扔,留下来继续用)。
换句话说,敏捷的“先写用例后写代码”,实际上是以单元测试代替详细设计文档、以高质量的面向对象的类定义代替模块设计、总体设计文档,从而把写文档的时间直接拿来写代码——然后借助面向对象框架直接导出文档就行了。

显然,敏捷模式对工程师的要求非常高。因此,最初搞敏捷的那些人甚至提倡“结对编程”,就是让两个工程师用一台电脑,一个写一个看。据说效率和软件质量比起单人分别开发都更好一些。
但这样太理想化。因为它要求两个工程师水平得差不多,不然就成了一个人写另一个干看,甚至看都看不懂。
不仅如此,它还要求这两个工程师都得有总体设计能力、可以一步到位的做出优秀的模块分解设计——这个要求一般公司根本做不到:大多数公司里,能做总体设计/模块设计的工程师可能只有一个,甚至连一个都没有。
但没有这个能力、没有足够的质量,程序就会越改越乱、越改越烂,绝不会越改越符合客户需要。也就是敏捷不起来,搞成了行为艺术。

再后来,就不再搞结对编程了;而是改成“先写测试,后写代码”——说白了,详细设计文档没人想写,写文档的功夫就把代码写出来了;而且经常写完了文档,才发现实际实现时遇到了问题,还得回头改文档……
先写测试,其实就是定内部功能划分和接口设计。不然没法写测试用例。写完测试再写代码,写出来就能马上跑一下测试,确保接口正确——倘若发现了问题,那就改测试用例(也就是改接口):反正总是要测的,改一下很自然,不多费工。
换句话说,现在只要求普通程序员有详细设计的能力,那就足够玩敏捷了——至于模块设计、总体设计、需求等等,那当然还是得专人来做。
相比之前,这显然就实际多了。

但我们现在大部分人、大部分公司玩的所谓“敏捷”呢?
简单说就是我也不会总体设计我也不懂详细设计,爱做啥样做啥样,做完了给客户碰运气,碰过去了喝啤酒,碰不过去加班再改——反正客户有花不完的钱!