當開發同事辭職,接手到垃圾代碼怎麼辦?

同事辭職了,自己接手到,他屎一樣的代碼怎麼辦 ,而且還要在屎一樣的代碼上面修改。我也是醉了。


當別人給你一瓢糞,你有三種選擇:

1.凈化它

2.直接潑給別人

3.攪幾下再潑給別人


昨晚剛吃了一晚上屎。

同事離職的時候,跟我說:這塊代碼已經好幾個月沒更新了很穩定沒啥事。

於是我真的以為沒啥事,沒有認真去看代碼。

結果呢,他走了,這塊代碼天天出問題。每天都有運維、客戶跑來讓我排查問題,

我真是***了。

昨天又有大問題爆發了,領導讓我先改改這坨shi。於是我打開ide,代碼load出來,整個人都蒙蔽了。

一個成員Bean類成員變數 + get/set 方法有500行 (定義了將近50個成員變數),大哥你就不能用Map來存儲一些相似的嗎。

隨處亂飛的magic Number。 一會來個

bean.getParamType() == 2

一會來個

if (errCode == 5)

大哥。。你就不能定義個枚舉嗎? 你這樣我得一直去根據上下文來推斷你到底想幹嘛啊!

還有各種代碼陋習。。。

比如:

List& list = new ArrayList();
List list = new ArrayList();

大哥。。我們就不能把泛型寫好看點嗎?你用IDEA 一片黃黃的你不揪心嗎!!?

沒有任何單元測試。。改完代碼我心慌慌。

沒有任何設計模式的影子,純粹的if else 與多個將近100行的switch堆砌起來。

switch方法開頭前還不忘寫了一句注釋: 手動編寫

大哥。。就不能用用狀態模式,多態來簡化下嗎?你這樣讓領導經常得讓我改代碼啊!

最後,還隱藏著多個bug。。。

不說了,心好累

(捂臉哭.jpg)

說到最後還是要正面回答下:

當程序員同事辭職,自己接手到屎一樣的代碼怎麼辦?

還能怎麼辦,領導說這塊代碼不要了,那你就偷爽吧,留著讓歲月與果蠅去吃了它,否則,你就只能自己慢慢吃了它吧!

(哭.jpg)


多圖預警,流量黨慎點。


16年做iOS的時候(現在Web前端),同事離職,接了個鍋。

為了增加可信度,我今天特意在git上把代碼clone了下來(居然還可以!!),雖然跑不起來了。

下面是正文:


1、跨界面傳值:傳值是怎麼做的呢?,什麼Delegate,blcok,通知 全都是垃圾,
全部寫在AppDelegate文件里,反正就當做全局的,在哪都可以用。

2、代碼行數(吃鯨!),代碼行數(害怕!),代碼行數(淚目!)
一個界面對應一個Controller,沒有Model,沒有view,完全看不到任何設計模式的影子,一個Controller類2千多行是我看到最少的了,首頁1w7行。

那個,那個,請。請。。請收下我的膝蓋。(淚目滿面!)


3、變數命名:(不說了自己看吧,)

這1、2、3、4什麼區別,看不懂啊。。

4、從來不寫注釋的,注釋這種無聊的東西只會浪費coding的時間吧

5、變數為何辣么多,原諒我悟性低,真的不知道為什麼要在viewDidLoad中初始化,也許全部用懶載入會使代碼行數更多吧,嗯,懶載入要8行代碼呢,這也是減少代碼行數的一種有效方法吧。

來載入的代碼是這樣的:

果然啊,這個同事還是比較機智的。


6、所有的視圖全部用StoryBoard拖的,幾十個界面全在一起亂的完全不知道是什麼。

這是界面,喵喵喵喵喵。。

我改了兩星期,跟領導談了至少五次,說我要重構或者重做,領導說時間不夠,直接改。

然後,接這個鍋三星期後我就離職了,直到現在,這個項目一直沒有發布新版本。現在App Store上的版本是我離職前發的那個版本。


一坨屎一樣的代碼,並不可怕。

我接過來兩千多萬行屎一樣的代碼,附加一個總是在反向努力的團隊附加一個從沒寫過java代碼的JAVA總架構師。。。。

怎麼辦。。。拿了工資獎金了,不幹也得干,我也不是那種眼裡揉的下沙子得過且過的人。

我的辦法很簡單,一年前,我提出一個theseus計劃,翻譯成中文叫做

《保持屎可以運行的同時將屎一點點換成黃金計劃》

有兩條路並行,一是屎的分層,屎有干屎稀屎很臭的有些臭的仔細聞聞還有點香的很多種,都分類,按照不同項目的進程,安排在不同時間點改。二是外骨骼架構,在屎外層構建一個殼,每當有部分功能需要增強,那麼不選擇在屎里增強,而整體在外骨骼架構裡面做一個新的,完成以後,替換屎內對應公司。這個外骨骼架構師純Spring的,因為想用市面上最多人懂得架構。打個比方,原來系統裡面的用戶認證非常簡單,全部寫死,CAS1.X+openldap的擴展,這個非常久遠了,那麼新需求來了以後,在Theseus骨骼上,增加一個system 「Cerberus」,重新設計,採用CAS 5.X+apahceDS的技術,以替換目前的東西。

你們啊,命好。現在的時代好了,做互聯網項目,能遇到多少代碼?組件部件都現成,我們傳統行業軟體,你能想像的最差,在我們這都能算是比較好的實現了, LOL


看到這個問題,想到的是同事接手到我的那一部分屎一樣的代碼,會講出什麼味兒的mmb。

前公司小,做外包,拿到過一個遊戲小外包,需求比畢業設計都簡單,用框架反而顯得臃腫,只需要在unity3D里隨便添加一點邏輯就能完成,幾個小時就能做完的幾萬塊的外包,你能想像這發包方有多外行。

也正因為外行,所以初始項目完成後,對方一看,覺得這裡可以加一個按鈕會更好,嗯,那裡再加個xx功能,反正都是小功能,你們程序員隨便動動手一下就完成了,對吧。

經過舌戰最終還是加了。

如此循環反覆數次,一個簡單的遊戲小項目已經淪為多功能的雜合體了。說她是遊戲吧,她又能賣實體產品,說她是商城吧,她又有AR功能,說她是AR吧,她居然還能當播放器,說她是播放器吧,她又能當社交軟體。

雖然功能上沒有問題,代碼無bug,但是每次要加新功能都有排山倒海的壓力。

直到我走了,這個項目還在與發包方進行激烈的協商,願目前拿到這坨代碼的同事身體健康,一切安好。


你需要調理。哥把葯煎好,別忘了起來喝。

藥引子

kai·zen

Kaizen is a lean manufacturing tool that improves quality, productivity, safety, and workplace culture. Kaizen focuses on applying small, daily changes that result in major improvements over time.

三碗煎成一碗

一日一花

在東日本大地震後,川瀨敏郎從2011年6月開始的一年,用古老、質樸、布滿歷史痕迹的器皿當做花器,依據時節到山野里找最當令的花葉,融入花器中,並持續在網上連載「一日一花」專欄,廣受好評後彙編成這一冊精美的圖鑑。366個作品野趣中充滿了寧靜和雅緻,在「空」和「寂」中給人一種生命的啟示。

茶之書

茶道是一種對「殘缺」的崇拜,是在我們都明白不可能完美的生命中,為了成就某種可能的完美,所進行的溫柔試探。

改善

本書是世界質量管理大師今井正明的代表作,揭示了日本戰後經濟迅速崛起的奧秘。改善的步伐雖然是漸進式、階梯式的,但是隨著生產質量和效率的持續提高,會帶來戲劇性的重大成果。同時,改善的不斷深入會讓企業擁有承受激進式變革的「強健體魄」。持續改善在生產製造領域威力巨大,用於改進銷售和服務流程也同樣效果驚人。

Working Effectively with Legacy Code

Get more out of your legacy systems: more performance, functionality, reliability, and manageability Is your code easy to change? Can you get nearly instantaneous feedback when you do change it? Do you understand it? If the answer to any of these questions is no, you have legacy code, and it is draining time and money away from your development efforts. In this book, Michael Feathers offers start-to-finish strategies for working more effectively with large, untested legacy code bases. This book draws on material Michael created for his renowned Object Mentor seminars: techniques Michael has used in mentoring to help hundreds of developers, technical managers, and testers bring their legacy systems under control. adding features, fixing bugs, improving design, optimizing performance Getting legacy code into a test harness Writing tests that protect you against introducing new problems Techniques that can be used with any language or platform

重構:改善既有代碼的設計

本書清晰揭示了重構的過程,解釋了重構的原理和踐方式,並給出了何時以及何地應該開始挖掘代碼以求改善。書中給出了70多個可行的重構,每個重構都介紹了一種經過驗證的代碼變換手法的動機和技術。本書提出的重構準則將幫助你一次一小步地修改你的代碼,從而減少了開發過程中的風險。

每天改善一點點。


你曉不曉得,你離職之後,後面接手你代碼的人,想法跟你驚人的相似?


曾經接到一個項目,運行沒有任何問題,改了再編譯就死活結果不對,後來逐步尋找代碼問題,跳轉幾十個文件後,到達某庫,裡面的代碼就是下面這種:

int func(type para1, type para2)

{

//注釋1

//注釋2

return 1;

}

沒錯,他編譯一個可用的exe糊弄交接以後,把核心代碼都刪了,留了一堆沒什麼用的代碼,留了一堆注釋!!!

據說因為不給應得的項目獎金,後來我甩鍋跑了。


吃別人拉的屎不可怕,可怕的是吃著吃著才發現原來這坨屎原來是以前自己拉的。。。


基本幾條路:

1. 說服自己,靜下心來,一定要靜下來(個人經歷),看懂代碼,弄懂了之後再重構與優化,其實發現也不那樣難受

2. 動動腦,動動嘴,推了

3. 自己也跑路,眼不見,心不煩


遇到這種情況只有兩種選擇:遞上辭職信,留下來接受它。

既然選擇接手「垃圾」項目代碼,也不能馬上就推倒重來,不是一朝一夕,以完成任務為主。重構需要對需求的充分理解,還要足夠的測試用例保證,否則可能導致推遲發布版本,而且伴隨著一堆 Bug。被領導問話去了,還懷疑你的代碼質量,以為你找了一堆借口。

其實每個人都會經歷夠相類似的階段,就算現在回過頭看看前幾年自己的編碼風格,有時候會發現很多待改進的地方,也無力去吐槽。

所以,跟著斷點Debug走,最好能夠充分理解代碼業務邏輯,完成任務為主,優化為輔。在新功能上就可以用上自己的新框架。


至少你得先看一看,說出不合理的地方,說出它爛在哪裡,有理有據,先說服我們,再說服你的老闆就不會太難。老闆不是傻子,真的做不下去了,他也不會做浪費勞動力的事情。你覺得呢?


每次聽到團隊成員說老代碼跟屎一樣想重寫,都會問一句:你怎麼確保你的代碼在更好一點的程序員眼裡就不是一坨屎了?

架構工作里一般解耦佔大頭,先從影響性能或可靠性的的局部開始剝離,每次一個小單元逐漸去重構會更安全和更快入手,並且不會產生業務發展的空窗期。 此外,開始前一定要先了解一下業務得重點保障的環節是哪些,不是所有東西都要搞掉的。


看別人的代碼很容易看成屎一樣。看的多了,也就不覺得屎了。

這完全取決於你的態度。如果積極一點,慢慢重構。如果消極一點,修修補補。重構對你來說應該是最好的選擇,過程中可以學到很多東西,雖然會累一點。


這有點不對題,因為不是離職的同事傳下來的。但這個代碼的奇葩性(如果HTML也算代碼的話),我一直很想找個機會來吐槽。

先說明一下,這個代碼是2016年的。對,不是1996年,不是2006年,是2016年。

這個網站是前公司的活動頁面。不是面向內部,是面向普通吃瓜群眾的。具體為一年一度舉辦的漫展的...官方頁面。

前情提要一下,漫展的組織者有我前公司,還有各種友商。各個友商也有負責不同的任務,所以官方網站就外包給了某個友商做了(當然不是免費做 -- 我們付了很多很多錢)。

出來後,我就覺得看上去... 有點挫。Logo的解析度有點低,導航欄的字體有點怪,配色有點...難看。按以前我肯定去Console看下代碼了。不過那段時間每天忙得頭冒青煙,這個不關我事就沒去細究。想著可能我自己要求比較高吧。你們覺得 OK 就 OK 吧。

一般網站外包的話,如果有什麼需要更新,得發郵件給他們,然後他們來更新(當然這個也不歸我管)。理論上,我前公司也沒有任何一個人負責技術。

離活動越來越近了,需要的更新越來越頻繁。但每次郵件過去,少則幾天,多則兩周才能更新(最多就是幾張圖片幾段文字)。性格很好的上司最後也忍不下去了,問他們要來了伺服器賬號。

然後,這種事就丟給我了。

我終於不得不去審視一下裡面的代碼。

沒看代碼之前,我覺得,人家肯定是專業的,既然能收這麼多錢。我想像的是應該科班出身、各種炫酷的前端技術,我懷疑我能不能看明白。

點開文件夾以後...

哇擦,好多文件,果然專。。。

等等,這不是前年(2015年)官方頁面的文件么?

兩年的網站完全沒任何關係,但是文件全部揉雜在一起。

再一看,一摞子的 xxxx.html...

我有一種不詳的預感。

打開index.html...

映入眼帘的是大片的&, &, &... 我截一下圖給大家感受一下:

(當年沒有存下來,現在官網已經換成今年的了。Web Archive上還能看到。)

我倒吸一口冷氣。

我進了官網隨意點了下頁面... 果真,這麼多頁面導航欄上的按鈕都是不統一的(之前忙的都懶得去看),因為如果要加一個減一個按鈕,得打開40多個在不同文件夾的 html 文件一個一個改...

再看看其他頁面的代碼,從頭 & 到尾, & 嵌套 &

再次提醒下,這是 2016 年的網頁,不是互聯網剛出來時候的網頁。

導航欄的按鈕上的文字... 是圖片。字體一看就是... 某日文字體的西文部分。(後來我們改成文字了)。

我崩潰的接下了這個頁面,一度糾結要不要拿 PHP 重構一下...

然後想了想,我不該這麼勤快,又沒多給我錢...

我最後就只是把要更新的頁面先更新了(理論上,還先需要發給版權商,版權商覺得沒問題了才能發布——當然之前基本上做不到)。

後來是我跟日本本社的一個同事負責更新。實在看不下去的陸陸續續稍微改了改,其他的就... 不管了。反正又沒多給我錢。

網站原來花了多少錢呢?反正比我一個月工資要多。

我非常不介意我來幫忙做。我不需要這麼高的價位,就給我相當於我一個月工資的錢好了,我可以花個周末半天的時間來幫忙做整個網站——絕對比這個好100倍。


垃圾項目其實是日積月累而成的,所謂冰凍三尺非一日之寒,豆腐渣工程絕對不是一塊兩塊磚的問題。

首先,極有可能從需求分析開始爛,因為用戶是不知道開發難度的,我以前說過軟體開發的用戶眼裡,用戶認為滿漢全席和家常小炒難度一樣。

同時最重要的問題是,用戶不清楚炒菜先放啥後放啥。

因此軟體項目從需求分析開始就不能是用戶方為主,用戶方可以提出自己的需求,同時開發方要引導用戶去雙方合作來完成,而不能僅是用戶方說了算。


重構啊,從此周報有得寫了


一,繼續吃別人拉的屎。二,自己重新拉一坨,雖然味道不見的比上一坨好,但畢竟是自己拉的,不嫌棄。


很認真的來回答這個問題。

個人認為,能夠耐心的處理混亂的歷史遺留代碼,是成為一個優秀的程序員的必要不充分條件。一味的想著推翻重寫和一心想要推翻X黨統治的憤青們從本質上講,是一樣的。你重新建立起來的並不會比現有的更好。

說現有東西爛,不願意在現有基礎上進行維護,修改,整理,優化的。只能說明你沒有這個能力,不要找借口。

另外,告訴大家一個秘密:優秀的擦屎能力真的是一條發財致富的好途徑


覺得可以把我半年前寫的一篇文章拿出來。雖然不能很好地回答「接手到屎一樣的代碼怎麼辦」這個問題,但至少勉強能夠回答「屎一樣的代碼是如何寫出的」或者「如何避免寫出屎一樣的代碼」的問題。

博客原文傳送門 一葉齋|編寫「可讀」代碼的實踐

~~~~~ 正文分割線 ~~~~~

編寫「可讀」代碼的實踐

編寫**可讀**的代碼,對於以代碼謀生的程序員而言,是一件極為重要的事。從某種角度來說,代碼最重要的功能是**能夠被閱讀**,其次才是**能夠被正確執行**。一段無法正確執行的代碼,也許會使項目延期幾天,但它造成的危害只是暫時和輕微的,畢竟這種代碼無法通過測試並影響最終的產品;但是,一段能夠正確執行,但缺乏條理、難以閱讀的代碼,它造成的危害卻是深遠和廣泛的:這種代碼會提高產品後續迭代和維護的成本,影響產品的穩定,破壞團隊的團結(霧),除非我們花費數倍於編寫這段代碼的時間和精力,來消除它對項目造成的負面影響。

在最近的工作和業餘生活中,我對「如何寫出可讀的代碼」這個問題頗有一些具體的體會,不妨記錄下來吧。

JavaScript 是動態和弱類型的語言,使用起來比較「輕鬆隨意」,在 IE6 時代,輕鬆隨意的習慣確實不是什麼大問題,反而能節省時間,提高出活兒的速度。但是,隨著當下前端技術的快速發展,前端項目規模的不斷膨脹,以往那種輕鬆隨意的編碼習慣,已經成為項目推進的一大阻力。

## 變數命名

變數命名是編寫可讀代碼的基礎。只有變數被賦予了一個合適的名字,才能表達出它在環境中的意義。

命名必須傳遞足夠的信息,形如 `getData` 這樣的函數命名就沒能提供足夠的信息,讀者也完全無法猜測這個函數會做出些什麼事情。而 `fetchUserInfoAsync` 也許就好很多,讀者至少會猜測出,這個函數大約會遠程地獲取用戶信息;而且因為它有一個 `Async` 後綴,讀者甚至能猜出這個函數會返回一個 Promise 對象。

### 命名的基礎

通常,我們使用名詞來命名對象,使用動詞來命名函數。比如:

monkey.eat(banana) // the money eats a banana
const apple = pick(tree) // pick an apple from the tree

這兩句代碼與自然語言(右側的注釋)很接近,即使完全不了解編程的人也能看懂大概。

有時候,我們需要表示某種集合概念,比如數組或哈希對象。這時可以通過名詞的複數形式來表示,比如用 `bananas` 表示一個數組,這個數組的每一項都是一個 `banana`。如果需要特彆強調這種集合的形式,也可以加上 `List` 或 `Map` 後綴來顯式表示出來,比如用 `bananaList` 表示數組。有些單詞的複數形式和單數形式相同,有些不可數的單詞沒有複數形式(比如 data,information),這時我也會使用 `List` 等後綴來表示集合概念。

### 命名的上下文

變數都是處在**上下文**(作用域)之內,變數的命名應與上下文相契合,同一個變數,在不同的上下文中,命名可以不同。舉個例子,假設我們的程序需要管理一個動物園,程序的代碼里有一個名為 `feedAnimals` 的函數來餵食動物園中的所有動物

function feedAnimals(food, animals){
// ...
// 上下文中有 bananas, peaches, monkey 變數
const banana = bananas.pop();
if(banana){
monkey.eat(banana);
} else {
const peach = peaches.pop();
monkey.eat(peach);
}
// ...
}

負責餵食動物的函數 `feedAnimals` 函數的主要邏輯就是:用各種食物把動物園裡的各種動物餵飽。也許,每種動物能接受的食物種類不同,也許,我們需要根據各種食物的庫存來決定每種動物最終分到的食物,總之在這個上下文中,我們需要關心食物的種類,所以傳給 `money.eat` 方法的實參對象命名為 `banana` 或者 `peach`,代碼很清楚地表達出了它的關鍵邏輯:「猴子要麼吃香蕉,要麼吃桃子(如果沒有香蕉了)」。我們肯定不會這樣寫:

// 我們不會這樣寫
const food = bananas.pop();
if(food){
monkey.eat(food);
} else {
const food = peaches.pop();
monkey.eat(food);
}

`Monkey#eat` 方法內部就不一樣了,這個方法很可能是下面這樣的(假設 `eat` 是 `Monkey` 的基類 `Animal` 的方法):

class Animal{
// ...
eat(food){
this.hunger -= food.energy;
}
// ...
}

class Monkey extends Animal{
// ...
}

如代碼所示,「吃」這個方法的核心邏輯就是根據食物的能量來減少動物(猴子)自身的飢餓度,至於究竟是吃了桃子還是香蕉,我們不關心,所以在這個方法的上下文中,我們直接將表示食物的函數形參命名為 `food`。

想像一下,假設我們正在編寫某個函數,即將寫一段公用邏輯,我們會選擇去寫一個新的功能函數來執行這段公用邏輯。在編寫這個新的功能函數過程中,往往會受到之前那個函數的影響,變數的命名也是按照其在之前那個函數中的意義來的。雖然寫的時候不感覺有什麼阻礙,但是讀者閱讀的單元是函數(他並不了解之前哪個函數),會被深深地困擾。

### 嚴格遵循一種命名規範的收益

如果你能夠時刻按照某種嚴格的規則來命名變數和函數,還能帶來一個潛在的好處,那就是你再也不用**記住**哪些之前命名過(甚至其他人命名過)的變數或函數了。特定上下文中的特定含義只有一種命名方式,也就是說,只有一個名字。比如,「獲取用戶信息」這個概念,就叫作 `fetchUserInfomation`,不管是在早晨還是傍晚,不管你是在公司還是家中,你都會將它命名為 `fetchUserInfomation` 而不是 `getUserData`。那麼當你再次需要使用這個變數時,你根本不用翻閱之前的代碼或依賴 IDE 的代碼提示功能,你只需要再**命名**一下「獲取用戶信息」這個概念,就可以得到 `fetchUserInfomation` 了,是不是很酷?

## 分支結構

分支是代碼里最常見的結構,一段結構清晰的代碼單元應當是像二叉樹一樣,呈現下面的結構。

if (condition1) {
if (condition2) {
...
} else {
...
}
} else {
if (condition3) {
...
} else {
...
}
}

這種優美的結構能夠幫助我們在大腦中迅速繪製一張圖,便於我們在腦海中模擬代碼的執行。但是,我們大多數人都不會遵循上面這樣的結構來寫分支代碼。以下是一些常見的,在我看來可讀性比較差的分支語句的寫法:

### 不好的做法:在分支中 return

function foo(){
if(condition){
// 分支1的邏輯
return;
}
// 分支2的邏輯
}

這種分支代碼很常見,而且往往分支2的邏輯是先寫的,也是函數的主要邏輯,分支1是後來對函數進行修補的過程中產生的。這種分支代碼有一個很致命的問題,那就是,如果讀者沒有注意到分支1中的 `return`(我敢保證,在使用 IDE 把代碼摺疊起來後,沒人能第一時間注意到這個 `return`),就不會意識到後面一段代碼(分支 2)是有可能不會執行的。我的建議是,把分支2放到一個 `else` 語句塊中,代碼就會清晰可讀很多:

function foo(){
if(condition){
// 分支 1 的邏輯
} else {
// 分支 2 的邏輯
}
}

如果某個分支是空的,我也傾向於留下一個空行,這個空行明確地告訴代碼的讀者,如果走到這個 `else`,我什麼都不會做。如果你不告訴讀者,讀者就會產生懷疑,並嘗試自己去弄明白。

### 不好的做法:多個條件複合

if (condition1 condition2 condition3) {
// 分支1:做一些事情
} else {
// 分支2:其他的事情
}

這種代碼也很常見:在若干條件同時滿足(或有任一滿足)的時候做一些主要的事情(分支1,也就是函數的主邏輯),否則就做一些次要的事情(分支2,比如拋異常,輸出日誌等)。雖然寫代碼的人知道什麼是主要的事情,什麼是次要的事情,但是代碼的讀者並不知道。讀者遇到這種代碼,就會產生困惑:分支2到底對應了什麼條件?

在上面這段代碼中,三種條件只要任意一個不成立就會執行到分支2,但這其實**本質**上是多個分支:1)條件1不滿足,2)條件1滿足而條件2不滿足,3)條件1和2都滿足而條件3不滿足。如果我們籠統地使用同一段代碼來處理多個分支,那麼就會增加閱讀者閱讀分支2時的負擔(需要考慮多個情況)。更可怕的是,如果後面需要增加一些額外的邏輯(比如,在條件1成立且條件2不成立的時候多輸出一條日誌),整個 `if-else` 都可能需要重構。

對這種場景,我通常這樣寫:

if(condition1){
if(condition2){
// 分支1:做一些事情
}else{
// 分支2:其他的事情
}
}else{
// 分支3:其他的事情
}

即使分支2和分支3是完全一樣的,我也認為有必要將其分開。雖然多了幾行代碼,收益卻是很客觀的。

萬事非絕對。對於一種情況,我不反對將多個條件複合起來,那就是當被複合的多個條件聯繫十分緊密的時候,比如 `if(foo foo.bar)`。

### 不好的做法:使用分支改變環境

let foo = someValue;
if(condition){
foo = doSomethingTofoo(foo);
}
// 繼續使用 foo 做一些事情

這種風格的代碼很容易出現在那些屢經修補的代碼文件中,很可能一開始是沒有這個 `if` 代碼塊的,後來發現了一個 bug,於是加上了這個 `if` 代碼塊,在某些條件下對 `foo` 做一些特殊的處理。如果你希望項目在迭代過程中,風險越積越高,那麼這個習慣絕對算得上「最佳實踐」了。

事實上,這樣的「補丁」積累起來,很快就會摧毀代碼的可讀性和可維護性。怎麼說呢?當我們在寫下上面這段代碼中的 `if` 分支以試圖修復 bug 的時候,我們內心存在這樣一個假設:我們是**知道**程序在執行到這一行時,`foo` 什麼樣子的;但事實是,我們根本**不知道**,因為在這一行之前,`foo` 很可能已經被另一個人所寫的嘗試修復另一個 bug 的另一個 if 分支所篡改了。所以,當代碼出現問題的時候,我們應當完整地審視一段獨立的功能代碼(通常是一個函數),並且多花一點時間來修復他,比如:

const foo = condition ? doSomethingToFoo(someValue) : someValue;

我們看到,很多風險都是在項目快速迭代的過程中積累下來的。為了「快速」迭代,在添加功能代碼的時候,我們有時候連函數這個最小單元的都不去了解,僅僅著眼於自己插入的那幾行,希望在那幾行中解決/hack掉所有問題,這是十分不可取的。

我認為,項目的迭代再快,其代碼質量和可讀性都應當有一個底線。這個底線是,當我們在修改代碼的時候,應當**完整了解當前修改的這個函數的邏輯**,然後**修改這個函數**,以達到添加功能的目的。注意,這裡的「修改一個函數」和「在函數某個位置添加幾行代碼」是不同的,在「修改一個函數」的時候,為了保證函數功能獨立,邏輯清晰,不應該畏懼在這個函數的任意位置增刪代碼。

## 函數

### 函數只做一件事情

有時,我們會自作聰明地寫出一些很「通用」的函數。比如,我們有可能寫出下面這樣一個獲取用戶信息的函數 `fetchUserInfo`:其邏輯是:

1) 當傳入的參數是用戶ID(字元串)時,返回單個用戶數據;

2) 而傳入的參數是用戶ID的列表(數組)時,返回一個數組,其中的每一項是一個用戶的數據。

async function fetchUserInfo(id){
const isSingle = typeof idList === "string";
const idList = isSingle ? [id] : id;
const result = await request.post("/api/userInfo", {idList});
return isSingle ? result[0] : resu< } // 可以這樣調用 const userList = await fetchUserInfo(["1011", "1013"]); // 也可以這樣調用 const user = await fetchUserInfo("1017");

這個函數能夠做兩件事:1)獲取多個用戶的數據列表;2)獲取單個用戶的數據。在項目的其他地方調用 `fetchUserInfo` 函數時,也許我們確實能感到「方便」了一些。但是,代碼的讀者一定不會有相同的體會,當讀者在某處讀到 `fetchUserInfo(["1011", "1013"])` 這句調用的代碼時,他就會立刻對 `fetchUserInfo` 產生「第一印象」:這個函數需要傳入用戶ID數組;當他讀到另外一種調用形式時,他一定會懷疑自己之前是不是眼睛花了。讀者並不了解背後的「潛規則」,除非規則是預先設計好並且及時地更新到文檔中。總之,我們絕不該一時興起就寫出上面這種函數。

遵循**一個函數只做一件事**的原則,我們可以將上述功能拆成兩個函數`fetchMultipleUser` 和 `fetchSingleUser` 來實現。在需要獲取用戶數據時,只需要選擇調用其中的一個函數。

async function fetchMultipleUser(idList){
return await request.post("/api/users/", {idList});
}

async function fetchSingleUser(id){
return await fetchMultipleUser([id])[0];
}

上述改良不僅改善了代碼的可讀性,也改善了可維護性。舉個例子,假設隨著項目的迭代,獲取單一用戶信息的需求不再存在了。

* 如果是改良前,我們會刪掉那些「傳入單個用戶ID來調用 `fetchUserInfo`」的代碼,同時保留剩下的那些「傳入多個用戶ID調用 `fetchUserInfo`」的代碼, 但是 `fetchUserInfo` 函數幾乎一定不會被更改。這樣,函數內部 `isSingle` 為 `true` 的分支,就留在了代碼中,成了永遠都不會執行的「臟代碼」,誰願意看到自己的項目中充斥著永遠不會執行的代碼呢?

* 對於改良後的代碼,我們(也許藉助IDE)能夠輕鬆檢測到 `fetchSingleUser` 已經不會被調用了,然後放心大膽地直接刪掉這個函數。

那麼,如何界定某個函數做的是不是**一件事情**?我的經驗是這樣:如果一個函數的參數僅僅包含**輸入數據(交給函數處理的數據)**,而沒有混雜或暗含有**指令**(以某種約定的方式告訴函數該怎麼處理數據),那麼函數所做的應當就是**一件事情**。比如說,改良前的 `fetchUserInfo` 函數的參數是「多個用戶的ID數組**或**單個用戶的ID」,這個「或」字其實就暗含了某種指令。

### 函數應適當地處理異常

有時候,我們會陷入一種很不好的習慣中,那就是,總是去嘗試寫出永遠不會報錯的函數。我們會給參數配上默認值,在很多地方使用 `||` 或者 `` 來避免代碼運行出錯,彷彿如果你的函數報錯會成為某種恥辱似的。而且,當我們嘗試去修復一個運行時報錯的函數時,我們往往傾向於在報錯的那一行添加一些兼容邏輯來避免報錯。

舉個例子,假設我們需要編寫一個獲取用戶詳情的函數,它要返回一個完整的用戶信息對象:不僅包含ID,名字等基本信息,也包含諸如「收藏的書籍」等通過額外介面返回的信息。這些額外的介面也許不太穩定:

async function getUserDetail(id){
const user = await fetchSingleUser(id);
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 上面這一行報錯了:Can not read property "books" of undefined.
// ...
}

假設 `fetchUserFavorites` 會時不時地返回 `undefined`,那麼讀取其 `books` 屬性自然就會報錯。為了修復該問題,我們很可能會這樣做:

const favorites = await fetchUserFavorits(id);
user.favoriteBooks = favorites favorites.books;
// 這下不會報錯了

這樣做看似解決了問題:的確,`getUserDetail` 不會再報錯了,但同時埋下了更深的隱患。

當 `fetchUserFavorites` 返回 `undefined` 時,程序已經處於一種異常狀態了,我們沒有任何理由放任程序繼續運行下去。試想,如果後面的某個時刻(比如用戶點擊「我收藏的書」選項卡),程序試圖遍歷 `user.favoriteBooks` 屬性(它被賦值成了`undefined`),那時也會報錯,而且那時排查起來會更加困難。

如何處理上述的情況呢?我認為,如果被我們依賴的 `fetchUserFavorits` 屬於當前的項目,那麼 `getUserDetail` 對此報錯真的沒什麼責任,因為 `fetchUserFavorits` 就不應該返回 `undefined`,我們應該去修復 `fetchUserFavorits`,任務失敗時顯式地告知出來,或者直接拋出異常。同時,`getUserDetail` 稍作修改:

// 情況1:顯式告知,此時應認為獲取不到收藏數據不算致命的錯誤
const result = await fetchUserFavorits(id);
if(result.success){
user.favoriteBooks = result.data.books;
} else {
user.favoriteBooks = []
}

// 情況2:直接拋出異常
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 這時 `getUserDetail` 不需要改動,任由異常沿著調用棧向上冒泡

那麼如果 `fetchUserFavorits` 不在當前項目中,而是依賴的外部模塊呢?我認為,這時你就該為選擇了這樣一個不可靠的模塊負責,在 `getUserDetail` 中增加一些「擦屁股」代碼,來避免你的項目的**其他部分**受到侵害。

const favorites = await fetchUserFavorits(id);
if(favorites){
user.favoriteBooks = favorites.books;
} else {
throw new Error("獲取用戶收藏失敗");
}

### 控制函數的副作用

無副作用的函數,是**不依賴上下文**,也**不改變上下文**的函數。長久依賴,我們已經習慣了去寫「有副作用的函數」,畢竟 JavaScript 需要通過副作用去操作環境的 API 完成任務。這就導致了,很多原本可以用純粹的、無副作用的函數完成任務的場合,我們也會不自覺地採取有副作用的方式。

雖然看上去有點可笑,但我們有時候就是會寫出下面這樣的代碼!

async function getUserDetail(id){
const user = await fetchSingleUserInfo(id);
await addFavoritesToUser(user);
...
}
async function addFavoritesToUser(user){
const result = await fetchUserFavorits(user.id);
user.favoriteBooks = result.books;
user.favoriteSongs = result.songs;
user.isMusicFan = result.songs.length &> 100;
}

上面,`addFavoritesToUser` 函數就是一個「有副作用」的函數,它改變了 `users`,給它新增了幾個個欄位。問題在於,僅僅閱讀 `getUserData` 函數的代碼完全無法知道,user 會發生怎樣的改變。

一個無副作用的函數應該是這樣的:

async function getUserDetail(id){
const user = await fetchSingleUserInfo(id);
const {books, songs, isMusicFan} = await getUserFavorites(id);
return Object.assign(user, {books, songs, isMusicFan})
}
async function getUserFavorites(id){
const {books, songs} = await fetchUserFavorits(user.id);
return {
books, songs, isMusicFan: result.songs.length &> 100
}
}

難道這不是理所當然的形式嗎?

### 非侵入性地改造函數

函數是一段獨立和內聚的邏輯。在產品迭代的過程中,我們有時候不得不去修改函數的邏輯,為其添加一些新特性。之前我們也說過,一個函數只應做一件事,如果我們需要添加的新特性,與原先函數中的邏輯沒有什麼聯繫,那麼決定是否通過**改造這個函數**來添加新功能,應當格外謹慎。

仍然用「向伺服器查詢用戶數據」為例,假設我們有如下這樣一個函數(為了讓它看上去複雜一些,假設我們使用了一個更基本的 `request` 庫):

const fetchUserInfo = (userId, callback) =&> {
const param = {
url: "/api/user",
method: "post",
payload: {id: userId}
};
request(param, callback);
}

現在有了一個新需求:為 `fetchUserInfo` 函數增加一道本地緩存,如果第二次請求同一個 userId 的用戶信息,就不再重新向伺服器發起請求,而直接以第一次請求得到的數據返回。

按照如下快捷簡單的解決方案,改造這個函數只需要五分鐘時間:

const userInfoMap = {};
const fetchUserInfo = (userId, callback) =&> {
if(userInfoMap[userId]){ // 新增代碼
callback(userInfoMap[userId]); // 新增代碼
}else{ // 新增代碼
const param = {
// ... 參數
};
request(param, (result)=&>{
userInfoMap[userId] = resu< // 新增代碼 callback(result); }); } }

不知你有沒有發現,經此改造,這個函數的可讀性已經明顯降低了。沒有緩存機制前,函數很清晰,一眼就能明白,加上新增的幾行代碼,已經不能一眼就看明白了。

實際上,「緩存」和「獲取用戶數據」完全是獨立的兩件事。我提出的方案是,編寫一個通用的緩存包裝函數(類似裝飾器)`memorizeThunk`,對 `fetchUserInfo` 進行包裝,產出一個新的具有緩存功能的 `fetchUserInfoCache`,在不破壞原有函數可讀性的基礎上,提供緩存功能。

const memorizeThunk = (func, reducer) =&> {
const cache = {};
return (...args, callback) =&> {
const key = reducer(...args);
if(cache[key]){
callback(...cache[key]);
}else{
func(...args, (...result)=&>{
cache[key] = resu< callback(...result); }) } } } const fetchUserInfo = (userInfo, callback) =&> {
// 原來的邏輯
}
const fetchUserInfoCache = memorize(fetchUserInfo, (userId)=&>userId);

也許實現這個方案需要十五分鐘,但是試想一下,如果將來的某個時候,我們又不需要緩存功能了(或者需要提供一個開關來打開/關閉緩存功能),修改代碼的負擔是怎樣的?第一種簡單方案,我們需要精準(提心弔膽地)地刪掉新增的若干行代碼,而我提出的這種方案,是以函數為單位增刪的,負擔要輕很多,不是嗎?

## 類的結構

### 避免濫用成員函數

JavaScript 中的類,是 ES6 才有的概念,此前是通過函數和原型鏈來模擬的。在編寫類的時候,我們常常忍不住地寫很多沒必要的成員函數:當類的某個成員函數的內部邏輯有點複雜了,行數有點多了之後,我們往往會將其中一部分「獨立」邏輯拆分出來,實現為類的另一個成員函數。比如,假設我們編寫某個 React 組件來顯示用戶列表,用戶列表的形式是**每兩個用戶為一行**

class UserList extends React.Component{
// ...
chunk = (users) =&> {
// 將 ["張三", "李四", "王二", "麻子"] 轉化為
// [["張三", "李四"], ["王二", "麻子"]]
}
render(){
const chunks = this.chunk(this.props.users);
// 每兩個用戶為一行
return (
&
{chunks.map(users=&>
&
{users.map(user =&>
&&& )}
&

)}
& )
}
}

如上述代碼所示,`UserList` 組件按照「兩個一行」的方式來顯示用戶列表,所以需要先將用戶列表進行組合。進行組合的工作這件事情看上去是比較獨立的,所以我們往往會將 `chunk` 實現成 `UserList` 的一個成員函數,在 render 中調用它。

我認為這樣做並不可取,因為 chunk 只會被 render 所調用,僅僅服務於 render。閱讀這個類源碼的時候,讀者其實只需要在 render 中去了解 chunk 函數就夠了。然而 chunk 以成員函數的形式出現,擴大了它的可用範圍,提前把自己曝光給了讀者,反而會造成干擾。讀者閱讀源碼,首先就是將代碼摺疊起來,然後他看到的是這樣的景象:

class UserList extends React.Component{
componentDidMount(){...}
componentWillUnmount(){...}
chunk(){...} // 讀者的內心獨白:這是什麼鬼?
render(){...}
}

熟悉 React 的同學對組件中出現一個不熟悉的方法多半會感到困惑。不管怎麼說,讀者肯定會首先去瀏覽一遍這些成員函數,但是閱讀 `chunk` 函數帶給讀者的信息基本是零,反而還會干擾讀者的思路,因為讀者現在還不知道用戶列表需要以「每兩個一行」的方式呈現。所以我認為,`chunk` 函數絕對應該定義在 `render` 中,如下所示:

render(){
const chunk = (users) =&> ...
const chunks = this.chunk(this.props.users);
return (
&
...
}

這樣雖然函數的行數可能會比較多,但將代碼摺疊起來後,函數的邏輯則會非常清楚。而且,`chunk` 函數曝光在讀者眼中的時機是非常正確的,那就是,在它即將被調用的地方。實際上,在「計算函數的代碼行數」這個問題上,我會把內部定義的函數視為一行,因為函數對讀者可以是黑盒,它的負擔只有一行。

## 總結

偉大的文學作品都是建立在廢紙堆上的,不斷刪改作品的過程有助於寫作者培養良好的「語感」。當然,代碼畢竟不是藝術品,程序員沒有精力也不一定有必要像作家一樣反覆**打磨**自己的代碼/作品。但是,如果我們能夠在編寫代碼時稍稍多考慮一下實現的合理性,或者在添加新功能的時候稍稍回顧一下之前的實現,我們就能夠培養出一些「代碼語感」。這種「代碼語感」會非常有助於我們寫出高質量的可讀的代碼。


推薦閱讀:

工程師應該如何保證代碼質量?
你們的開發團隊有引入findbugs等代碼檢測工具嗎?老代碼改不改嗎?

TAG:程序員 | 代碼 | 辭職 | 代碼質量 |