您正在閱讀我關於乾淨程式碼的書「清洗程式碼」的摘錄。提供 PDF、EPUB、平裝本和 Kindle 版本。立即取得副本。
聰明的程式碼是我們在工作面試問題或語言測驗中可能會看到的東西,當他們希望我們知道我們以前可能從未見過的語言功能是如何工作的。我對所有這些問題的回答是:「它不會通過程式碼審查」。
有些人將簡潔與清晰度混淆。短代碼(簡潔)並不總是最清晰的程式碼(清晰),通常情況恰恰相反。努力讓程式碼更短是一個崇高的目標,但它不應該以犧牲可讀性為代價。
在程式碼中表達相同想法的方法有很多種,有些比其他更容易理解。我們應該始終致力於減少下一個閱讀我們程式碼的開發人員的認知負擔。每當我們偶然發現一些不太明顯的東西時,我們就浪費了我們的大腦資源。
資訊:我從 Steve Krug 的同名網路可用性書中「竊取」了本章的名稱。
讓我們來看一些例子。試著涵蓋答案並猜測這些程式碼片段的作用。然後,數數你猜對了多少個。
範例1:
const percent = 5; const percentString = percent.toString().concat('%');
此程式碼僅將 % 符號加到數字中,應重寫為:
const percent = 5; const percentString = `${percent}%`; // → '5%'
範例2:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
~ 符號稱為 位元 NOT 運算子。它在這裡的有用作用是,只有當 indexOf() 傳回 -1 時,它才會傳回一個假值。這段程式碼應該重寫為:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
範例 3:
const value = ~~3.14;
位元 NOT 運算子的另一個晦澀用法是丟棄數字的小數部分。使用 Math.floor() 代替:
const value = Math.floor(3.14); // → 3
範例 4:
if (dogs.length + cats.length > 0) { // Something fishy here… }
這個很快就可以理解了:它檢查兩個數組中的任何一個是否有任何元素。不過,最好說清楚一點:
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
範例 5:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
這個問題我花了一段時間才明白。假設我們有 URL 的一部分,例如 filename="pizza"。首先,我們用 = 分割字串並取得第二部分「pizza」。然後,我們將第一個和最後一個字元切片以獲得披薩。
我可能會在這裡使用正規表示式:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
或者更好的是 URLSearchParams API:
const header = 'filename="pizza.rar"'; const filename = new URLSearchParams(header) .get('filename') .replaceAll(/^"|"$/g, ''); // → 'pizza'
不過這些引用很奇怪。通常我們不需要在 URL 參數周圍加上引號,因此與後端開發人員交談可能是個好主意。
範例 6:
const percent = 5; const percentString = percent.toString().concat('%');
在上面的程式碼中,當條件為真時,我們會為物件新增一個屬性,否則我們什麼都不做。當我們明確定義要解構的物件而不是依賴虛假值的解構時,意圖更加明顯:
const percent = 5; const percentString = `${percent}%`; // → '5%'
我通常更喜歡物件不改變形狀,所以我會移動值欄位內的條件:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
範例7:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
這個奇妙的單行程式碼創造了一個充滿從 0 到 9 的數字的陣列。 Array(10) 創建了一個包含10 個空 元素的數組,然後keys() 方法返回鍵(從0 開始的數字)到9) 作為迭代器,然後我們使用擴展語法將其轉換為普通數組。爆炸頭表情符號…
我們可以使用 for 迴圈來重寫它:
const value = ~~3.14;
儘管我喜歡避免程式碼中出現循環,但循環版本對我來說更具可讀性。
中間的某個地方將使用 Array.from() 方法:
const value = Math.floor(3.14); // → 3
Array.from({length: 10}) 建立一個包含 10 個 未定義 元素的數組,然後使用 map() 方法,我們用 0 到 9 的數字填入該數組。
我們可以使用 Array.from() 的映射回呼將其寫得更短:
if (dogs.length + cats.length > 0) { // Something fishy here… }
明確的map()可讀性稍好一些,我們不需要記住Array.from()的第二個參數的作用。此外,Array.from({length: 10}) 比 Array(10) 更具可讀性。雖然只有一點點。
那麼,你的分數是多少?我想我的應該是3/7左右。
有些模式介於聰明和可讀性之間。
例如,使用布林值過濾掉虛假的陣列元素(本例為 null 和 0):
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
我覺得這種模式可以接受;雖然它需要學習,但它比其他選擇好:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
但是,請記住,這兩種變體都會過濾掉 falsy 值,因此如果零或空字串很重要,我們需要明確過濾未定義或 null:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
當我看到兩行看起來相同的棘手程式碼時,我認為它們在某些方面有所不同,但我還沒有看到差異。否則,程式設計師可能會為重複的程式碼建立一個變數或函數,而不是複製貼上它。
例如,我們有一段程式碼可以為我們在專案中使用的兩個不同工具(Enzyme 和 Codeception)產生測試 ID:
const header = 'filename="pizza.rar"'; const filename = new URLSearchParams(header) .get('filename') .replaceAll(/^"|"$/g, ''); // → 'pizza'
很難立即發現這兩行程式碼之間的差異。還記得那些需要找出十個不同點的圖片嗎?這就是這段程式碼對讀者的作用。
雖然我通常對極端的程式碼乾燥持懷疑態度,但這是一個很好的例子。
訊息:我們在分而治之或合併與放鬆章節中詳細討論了「不要重複自己」原則。
const percent = 5; const percentString = percent.toString().concat('%');
現在,毫無疑問,兩個測試 ID 的程式碼是完全相同的。
讓我們來看一個更棘手的例子。假設我們對每個測試工具使用不同的命名約定:
const percent = 5; const percentString = `${percent}%`; // → '5%'
這兩行程式碼之間的差異很難注意到,而且我們永遠無法確定名稱分隔符號(- 或 _)是這裡唯一的差異。
在有這樣需求的項目中,這種模式很可能會出現在很多地方。改進它的一種方法是創建為每個工具產生測試 ID 的函數:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
這已經好多了,但還不夠完美——重複的程式碼仍然太大。讓我們也解決這個問題:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
這是使用小函數的極端情況,我通常會盡量避免如此拆分程式碼。然而,在這種情況下,它效果很好,特別是如果專案中已經有很多地方我們可以使用新的 getTestIdProps() 函數。
有時,看起來幾乎相同的程式碼有細微的差異:
const value = ~~3.14;
這裡唯一的區別是我們傳遞給函數的參數具有很長的名稱。我們可以在函數呼叫中移動條件:
const value = Math.floor(3.14); // → 3
這消除了類似的程式碼,使整個程式碼片段更短且更易於理解。
每當我們遇到使程式碼略有不同的條件時,我們應該問自己:這個條件真的有必要嗎?如果答案是“是”,我們應該再問自己一次。通常,對於特定條件並沒有真正需求。例如,為什麼我們甚至需要為不同的工具分別新增測試ID?我們不能配置其中一個工具來使用另一個工具的測試 ID 嗎?如果我們挖掘得夠深,我們可能會發現沒有人知道答案,或者最初的原因不再相關。
考慮這個例子:
if (dogs.length + cats.length > 0) { // Something fishy here… }
此程式碼處理兩種邊緣情況:當 assetDir 不存在時,以及當 assetDir 不是陣列時。此外,物件產生程式碼是重複的。 (我們不要談論嵌套三元組...)我們可以擺脫重複和至少一個條件:
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
我不喜歡 Lodash 的castArray() 方法將 undefined 包裝在陣列中,這不是我所期望的,但結果仍然更簡單。
CSS 具有簡寫屬性,開發人員經常過度使用它們。這個想法是單一屬性可以同時定義多個屬性。這是一個很好的例子:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
等同於:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
一行程式碼而不是四行,仍然很清楚發生了什麼:我們在元素的所有四個邊上設定了相同的邊距。
現在,看這個例子:
const percent = 5; const percentString = percent.toString().concat('%');
要了解他們的作用,我們需要知道:
這會造成不必要的認知負擔,並使程式碼更難以閱讀、編輯和審查。我避免使用這種簡寫。
速記屬性的另一個問題是它們可以為我們不打算更改的屬性設定值。考慮這個例子:
const percent = 5; const percentString = `${percent}%`; // → '5%'
該聲明設定了 Helvetica 字體系列,字體大小為 2rem,並使文字變為斜體和粗體。我們在這裡沒有看到的是,它還將行高更改為預設值正常。
我的經驗法則是僅在設定單一值時才使用簡寫屬性;否則,我更喜歡手寫屬性。
這裡有一些很好的例子:
const url = 'index.html?id=5'; if (~url.indexOf('id')) { // Something fishy here… }
以下是一些需要避免的例子:
const url = 'index.html?id=5'; if (url.includes('id')) { // Something fishy here… }
雖然簡寫屬性確實使程式碼更短,但它們通常會使其難以閱讀,因此請謹慎使用它們。
消除條件並不總是可能的。但是,有一些方法可以使程式碼分支中的差異更容易被發現。我最喜歡的方法之一就是我所說的平行編碼。
考慮這個例子:
const value = ~~3.14;
這可能是我個人的煩惱,但我不喜歡返回語句處於不同的級別,這使得它們更難以比較。讓我們加入一個 else 語句來解決這個問題:
const value = Math.floor(3.14); // → 3
現在,兩個回傳值都處於相同的縮排級別,使它們更容易比較。當沒有條件分支處理錯誤時,此模式有效,在這種情況下,儘早返回將是更好的方法。
訊息:我們在避免條件章節中討論提前退貨。
這是另一個例子:
if (dogs.length + cats.length > 0) { // Something fishy here… }
在此範例中,我們有一個按鈕,其行為類似於瀏覽器中的鏈接,並在應用程式中顯示確認模式。 onPress 屬性的相反條件使這個邏輯很難看出。
讓兩個條件都為正:
if (dogs.length > 0 && cats.length > 0) { // Something fishy here… }
現在,很明顯我們可以根據平台設定 onPress 或 link 屬性。
我們可以停在這裡或更進一步,這取決於組件中 Platform.OS === 'web' 條件的數量或我們需要有條件設定的 props
我們可以將條件道具提取到一個單獨的變數中:
const header = 'filename="pizza.rar"'; const filename = header.split('filename=')[1].slice(1, -1);
然後,使用它而不是每次都硬編碼整個條件:
const header = 'filename="pizza.rar"'; const filename = header.match(/filename="(.*?)"/)[1]; // → 'pizza'
我也會將 target 屬性移至 Web 分支,因為應用程式無論如何也不會使用它。
當我二十多歲的時候,記得事情對我來說並不是什麼問題。我可以回憶起我讀過的書以及我正在從事的專案中的所有功能。現在我四十多歲了,情況不再是這樣了。我現在看重不使用任何技巧的簡單程式碼;我重視搜尋引擎、快速存取文件以及幫助我推理程式碼和導航項目的工具,而無需將所有內容都記在腦子裡。
我們不應該為現在的自己寫程式碼,而應該為幾年後的自己寫程式。思考是困難的,而程式設計需要大量的思考,即使不需要破解棘手或不清楚的程式碼。
開始思考:
如果您有任何回饋,請發送給我、發推文、在 GitHub 上提出問題,或發送電子郵件至 artem@sapegin.ru。取得您的副本。
以上是清洗你的程式碼:別讓我思考的詳細內容。更多資訊請關注PHP中文網其他相關文章!