我见过的最糟糕代码
作者 | Mehdi Zed
在本文,將向讀者展示一些作者見過的一些最糟糕的代碼。除非你希望被同事和用戶討厭,否則這些“魔鬼”永遠(yuǎn)都不應(yīng)該被放到人間。我們會發(fā)現(xiàn),通過一些好的實(shí)踐,其實(shí)很容易避免它們。
**
01”魔鬼代碼“
需要改進(jìn)的代碼與所謂的“魔鬼代碼”是不一樣的。
不管使用的是哪種語言,“魔鬼代碼”都是糟糕的代碼,因?yàn)樗鼤<绊?xiàng)目的穩(wěn)定性和可維護(hù)性。告訴你,我見過很多“魔鬼代碼”。
當(dāng)它堆積如山時(shí),你的項(xiàng)目很快就會變成十八層地獄的樣貌。如果你喜歡到處捅婁子,那么團(tuán)隊(duì)領(lǐng)導(dǎo)看你的眼光也會越來越不一樣。
**
02模棱兩可和前后矛盾
很久很久以前,在一個(gè)遙遠(yuǎn)的星系中,我在一個(gè)清晨醒來,被世界末日般的景象嚇得跳了一跳。
生產(chǎn)環(huán)境出了一個(gè)了不得的錯(cuò)誤。生產(chǎn)中的所有系統(tǒng)票證莫名其妙地返回“null”。到處都亂成一團(tuán)。所有人都像無頭蒼蠅一樣到處亂跑。
我百米沖刺到我的工作站,第一個(gè)條件反射就是看 Kibana。沒有日志,什么都沒有。完蛋了,這可不是什么好的開始。
因此,我決定追溯票證的創(chuàng)建路徑。
為此,我必須深入研究系統(tǒng)自盤古開天辟地以來創(chuàng)建的那些內(nèi)部庫。考古工作結(jié)束的時(shí)候,我找到了問題來源之一的一堆文件。
然后,我看到了這樣的景象:
// use id and expire to get ticket async function get_ticket(i, expire) {return CheckisNotExp(expire).then(async function() {var t = await GetTicketModel(i)if (t) {return t} else {logger.error(JSON.stringify(t))return null}}).catch(async function(e) {logger.error(JSON.stringify(e))return null })}真的有一個(gè)“i”變量嗎?我們現(xiàn)在在哪?這是一個(gè) id,對不對?它是整數(shù)還是 UUID?到底是什么到期了?是日期還是時(shí)間戳?為什么會有 camelCase、PascalCase 和 snake_case?帶有 promise 的異步注解和又一個(gè)異步注解?如果失敗,我們會返回 null?簡直是魔鬼啊!
那時(shí),每隔 5 分鐘就有一半的公司同事向我發(fā) Skype 消息,索取 ETA 修復(fù)。
所以首先,有人需要知道這里究竟發(fā)生了什么。
我很快意識到,該為此負(fù)責(zé)的不是一個(gè)人,而是三個(gè)人。很久以前,其中兩個(gè)人離開了公司,而第三個(gè)人今天早上還沒來。這真是經(jīng)典場景。
根據(jù) Git 的記錄,這三個(gè)人碰這個(gè)文件的時(shí)間各自差了很久。因此他們留下了不一致的代碼、不同的樣式、不一樣的 ECMAScript 版本和不同的 promise 處理方式。
不管怎樣,在這段代碼中,一切都是模棱兩可的,一切都是不一致的。這是一個(gè)絕佳的反面案例,你應(yīng)該盡一切可能避免這種情況。不用說,代碼審查并沒有覆蓋到這里。
現(xiàn)在好了,我們必須快速重寫它。
我得更改那些變量和函數(shù)的名稱。不能再有歧義了。各處的 Async/Await 都要做成相同的方式。
我還要確保自己不會漏掉任何錯(cuò)誤,結(jié)果再返回一個(gè) null。如果出現(xiàn)了什么問題,這些錯(cuò)誤肯定要破壞函數(shù)。異常應(yīng)由上面的層來處理。
// hotfix // @todo rewrite the ticket module entirely async function getTicket(ticketUuid, ticketExpirationTimestamp) {await validTicketExpiration(ticketUuid, ticketExpirationTimestamp)const ticket = await getTicketByUuid(ticketUuid)return ticket }最好的解決方案是重寫這個(gè)模塊的一部分。這里的票證驗(yàn)證邏輯很糟糕。但這并不是最要緊的事情。
當(dāng)務(wù)之急是找出并修復(fù)錯(cuò)誤。
在更新代碼后,真正的錯(cuò)誤開始浮出水面。前一天所做的一個(gè)配置更改改變了票證創(chuàng)建行為。返回到先前的配置可以立即解決這個(gè)問題。
接下來的一周時(shí)間里,有問題的模塊被完全重寫。
**
03肉醬意面
只有一個(gè)依賴項(xiàng)
很久很久以前,在一個(gè)遙遠(yuǎn)的星系中,我正在做一個(gè)代碼干凈整齊的產(chǎn)品。
作為優(yōu)質(zhì)產(chǎn)品,一切都在內(nèi)部做好了優(yōu)化。功能是用盡可能少的代碼開發(fā)的。代碼高度重視可讀性。由注重整潔代碼的工程師管理的代碼審查流程確保了產(chǎn)品嚴(yán)格遵循所有最佳實(shí)踐。
SOLID、DRY、KISS、YAGNI 和你可以想到的其他首字母縮寫詞,這里都能見得到。
即使做到這個(gè)地步,這個(gè)產(chǎn)品的某個(gè)特殊部分也會間歇性地崩潰。在一個(gè)沖刺期間,我終于設(shè)法安排出了時(shí)間來調(diào)查這件事。
很快,我意識到問題不在于產(chǎn)品。那些錯(cuò)誤只有一個(gè)共同點(diǎn):一個(gè)依賴項(xiàng)。那是一個(gè)通過內(nèi)部工件處理的內(nèi)部依賴項(xiàng)。
它由另一個(gè)團(tuán)隊(duì)管理,而且——令人驚訝的是——這段代碼不是免費(fèi)提供的。你必須先獲得許可才能看到它。因此,我請求了訪問權(quán)限來了解到底發(fā)生了什么事情。
然后我收到了一條 slack 消息,問我為什么要訪問源碼。
“你好!為什么你需要訪問這個(gè)存儲庫?”
“你是什么意思?你知道我在這里工作嗎?等等,我在路上。”
我突然出現(xiàn)在他面前后,終于拿到了訪問該項(xiàng)目的權(quán)限。
我在其中看到一個(gè)文件,大小為 300KB。300KB 的文本,竟然有那么大。它已經(jīng)有好幾年沒人碰過了。上次碰過它的那個(gè)人,我完全不認(rèn)識。
簡直是最可怕的魔鬼。
那是我一生中見過的規(guī)模最大的意大利面條代碼。篇幅所限,我并沒有把所有代碼都放在這里。下面的代碼只是我看到的那一坨東西的冰山一角。
小心閱讀,它讀起來扎眼。
// Thousands of lines of spaghetti codesif (global.Builder) {module.exports.buildPgs = function(pgs, options, limitNodes = 0) {var config = options.config || {};var builded = [];pgs.each(function(pg) {var supported = pg.prop('tagName') == "INPUT"&& pr.attr['name'] == "file"&& options.pgs.rel.active == ""&& global.FileReader;if (!supported || !pg.f || pg.f.length == 0)return;for (var i = 0; i < pg.f.length; i++){builded.push({file: pg.f[i],instanceConfig: _.extend({}, config)});if (isFunction(options.before)){var returned = options.before(pg.f.path);if (typeof returned === 'object' && global.status.in_progress){if (returned.action == "skip"){var needsSkip = (typeof global.status.in_progress === 'boolean' && global.status.in_progress)|| _.hasAny("cancel", Global._quotes.BAD_DELIMITERS)|| str.indexOf(Global._delimiter) > -1;if(needsSkip) return;}else if (typeof returned.config === 'object'){var LOCAL_BUILDER = new global.Builder("/builder/" + options.module + "/" + options.module + );for(var s=p,a=p.matchIndex(o),shift=0,i=0;i<a.length;i++){var deepcopyfile = JSON.parse(JSON.stringify(pg.f[i].getRawValue()));LOCAL_BUILDER.build(deepcopyfile)LOCAL_BUILDER.onmessage = global.Notification("buildPg", deepcopyfile);}}}}}});} }// Thousands of lines of spaghetti codes我甚至都沒有嘗試去碰這個(gè)惡魔之子。
在這類情況下,解決方案不是從代碼中找出來的。我召開了一次小組會議,向他們解釋具體情況。我的計(jì)劃也很簡單。
我們用一個(gè)已經(jīng)可用的開源模塊替掉了這個(gè)撒旦般的依賴項(xiàng)。與往常一樣,這是一個(gè)大問題。必須做一些準(zhǔn)備工作才能正確插入新的依賴項(xiàng)。
一開始的快速調(diào)查已經(jīng)演變成持續(xù)幾天的一項(xiàng)艱巨任務(wù)。
在會議桌那頭,Scrum 主管很生氣。討論得越多,我越覺得想要不碰到該死的東西會非常困難。當(dāng)我展示我們的處境后,討論結(jié)束了,答案是不行。
“你只需要稍微動一動這個(gè)模塊,把它修好就行了,然后我們會繼續(xù)原本的工作。”
于是乎,我做了開發(fā)人員為代碼質(zhì)量和項(xiàng)目的可持續(xù)性應(yīng)該做的額外工作。我說不行。我甚至走得更遠(yuǎn)。這是我職業(yè)生涯中的第一次,也是唯一一次,如果我被迫要辭職,我已經(jīng)做好辭職的準(zhǔn)備。
他們顯然問了其他開發(fā)人員。大家都拒絕了。
由于這個(gè)問題的嚴(yán)重性,我爭取到替換這個(gè)模塊所需的時(shí)間。我為開源依賴項(xiàng)開發(fā)了一個(gè)小型適配器。然后我擺脫了那個(gè)被詛咒的依賴項(xiàng)。
此后,那個(gè)產(chǎn)品一切順利,運(yùn)行正常。
開發(fā)人員經(jīng)常會抱怨意大利面條代碼,這是有充分理由的。這是你能見過的最糟糕的代碼。但無需大量投資即可確保你避免這種情況。
**
04驅(qū)魔
一開始,本文想寫的是一個(gè)最佳實(shí)踐的列表。
“作為開發(fā)人員,為什么以及如何應(yīng)用最佳實(shí)踐。”
不過上面這個(gè)標(biāo)題很容易像大劑量安眠藥一般令人昏昏欲睡,此外我出于兩個(gè)原因更改了計(jì)劃。
首先,對于我,特別是對你來說,先談?wù)摵蠊麜腥ず芏唷﹂_發(fā)人員來說,這很重要,因?yàn)檫@就是魔鬼代碼的起源。此外,如果你可以為我的遭遇會心一笑,那也很好。
其次,互聯(lián)網(wǎng)上已經(jīng)有很多關(guān)于這個(gè)主題的文章。它們都有一個(gè)共同點(diǎn),就是它們的內(nèi)容都是從兩本書中摘出來的。這兩本書培養(yǎng)了幾代開發(fā)人員。——羅伯特·馬丁的《代碼整潔之道》、史蒂夫·麥康奈爾的《代碼大全》
你是否真的要縮短代碼審查時(shí)間,并且再也不想搞出什么魔鬼代碼?直接看原始資料就行,花點(diǎn)時(shí)間好好看完這兩本書。
我發(fā)現(xiàn)《代碼大全》的方法更易讀、更實(shí)用。但是,盡管《代碼整潔之道》非常復(fù)雜,但它教給我的知識不亞于甚至超過了《代碼大全》。前者里面使用的代碼是 Java 和 C++,但是誰在乎具體的語言呢?你在這本書里學(xué)到的是規(guī)則和編程理念。
用代碼審查來驗(yàn)證代碼是好事情。但是,如果你不確定為什么它是好的代碼,那么到頭來還是會出來你經(jīng)歷過的魔鬼代碼。
—END—更多精彩內(nèi)容歡迎關(guān)注百度開發(fā)者中心公眾號。
總結(jié)
- 上一篇: 可申请试用!GN4系列GPU云服务器重磅
- 下一篇: Kubernetes入门——深入浅出讲D