。再次提醒下,這是 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:程序員 | 代碼 | 辭職 | 代碼質量 |