本文最初發布于 hackernnon 網站,經原作者授權由 InfoQ 中文站翻譯并分享。
SOLID 原則是開發人員創建靈活、可理解和可維護代碼的基礎。但你要正確遵循這些原則就可能明顯減慢開發速度,并且大多數人沒那么操心代碼質量,因此我發明了一套更好用的原則。
DILOS 原則是我們構建可怕代碼的堅實支柱。
我個人已經用上了 DILOS 原則,成功創建出大堆混亂難懂和臃腫的代碼,這篇文章我就來具體介紹一下:
DILOS 的含義:
- D——依賴倒置反轉原則
- I——接口捆綁原則
- L——里氏分離原則
- O——開閉原則
- S——多職責原則
依賴倒置反轉原則
高級模塊必須依賴低級模塊。依賴實體而不是抽象。
把某些東西抽象出來,就是要隱藏這些東西內部的實現細節,有時是原型,有時是函數。因此當你調用這個函數時不必完全了解其機制。如果你非得先搞懂大型代碼庫中的所有函數,那就別想著寫代碼了。可能需要幾個月的時間才能看完那些東西。
但現在我們要把這條原則倒過來:不要抽象任何東西。也就是盡量少用小塊函數,把所有東西都塞到一個單體函數里。如果別人想調用你的函數,讓他看懂你的每一行代碼再說吧。
下面是整潔代碼的一個示例:
function hitAPI(url, httpMethods){
// Implementation example
}
hitAPI("https://www.kealanparr.com/retrieveData", "GET");
hitAPI("https://www.kealanparr.com/retrieveInitialData", "GET");
你看,你用不著操心 hitAPI 在做什么,我們只傳遞了一個 URL 和一個 HTTP 請求,然后就搞定了。現在這段代碼是高度可重用和可維護的。這個函數可以在一個地方處理所有 URL。我們已經盡可能讓高級函數(我們放在 base 原型中的函數,可以和下層的許多東西共享)不依賴于任何低級函數。
那么如果我們反轉這個依賴倒置(Dependency-Inversion)原則呢?
function hitDifferentAPI(type, httpMethods){
if (this instanceof initialLoad) {
// Implementation example
} else if (this instanceof navBar) {
// Implementation example
} else {
// Implementation example
}
}
現在我們讓高級 api 請求依賴于許多較低級別的類型。完成任務后,它不再是完全通用的了,并且會依賴其繼承鏈中較低的類型。
接口捆綁原則
強迫客戶端依賴它們不用的 [代碼]。
其他語言里的接口用于定義不同對象擁有的方法和屬性。
不要向不需要的對象添加代碼?不要將太多無關的功能捆綁在一起?嗯,胡扯嘛這是。
我們一定要把松散耦合的代碼都綁在一個地方。關鍵在于一定要依賴你用不著的東西。
我們稍后將在“多重職責原則”中進一步解釋,但請記住這條原則,在所有地方瘋狂用它。還記得花半天時間查找幾百個文件搜索 bug 的經歷嗎?那種事情不會再有了。搞一個名為 main.js 的 JS 文件,然后把所有代碼都塞進去。
讓你的站點預加載所有內容,不要搞什么 JS 腳本按需加載,這樣初始加載速度就會慢如蝸牛啦。
寫代碼的時候把宇宙毀滅時的需求都想好,然后提前寫好對應的邏輯,反正你遲早用得上嘛。
如果有人需要你代碼里的一根香蕉,那就塞給他一頭拿著香蕉的大猩猩。客戶端要啥就給它附送一堆垃圾,它們肯定會感謝你的。
寫的函數越少越好。把什么東西封裝在一個放在其他地方的新函數里,并抽象化它的邏輯?可別這么干。怎么讓人犯迷糊怎么來,需要代碼的時候復制粘貼過來就行。
理想情況下,我們的代碼流只有 1 個對象。在非常大的代碼庫中,我們可能有 2 個對象。通常將其稱為“上帝對象”反模式,其中我們要到處用單獨的一個對象,因為所有事情都得它來做。稍后我們將詳細討論。
里氏分離原則
軟件各部分的子級和父級不可以互換。
你竟然會在代碼中使用繼承嗎?這絕對要注意。你應該復制粘貼而不是繼承代碼。Copy-Paste 反模式就是這個意思,你不應該把代碼的通用功能抽象為模塊化的可重用功能,而應當在所有需要的地方都復制代碼。這會增加技術債(將來你遲早要回來修復的),而且每更改一段代碼,都需要多次搜索才能找到它在代碼庫中出現在了哪些位置。
DRY 原則表示 Don't Repeat Yourself,而 WET 原則恰恰相反,指的是我們 Write Everything Twice。必要時應該寫更多次數。抵制繼承,隨意復制粘貼。
但是,如果你確實需要利用里氏分離原則,請確保在與繼承鏈中較高子級(父級)交換對象時,繼承鏈中較低子級的對象原型不能正常工作。
為什么?
因為如果我們不遵循里氏分離原則,我們就會構建準確而健壯的繼承鏈。將邏輯抽象為封裝好的 base 原型 / 對象。我們還會對原型鏈中的不同方法按邏輯分組,它們的特定覆蓋會讓代碼路徑更加可預期和可發現。
如果你正確地遵循了這一原則,那么父級能用的時候子級也沒法用,繼承就會毫無意義。如果你的程序嘗試引用的函數并不存在于自己的子級中,因此崩潰掉——你就會完全避免繼承會給你帶來的任何好處——這正是我們遵循這一原則的目的。
開閉原則
對象應對修改開放,對擴展封閉。
好的代碼通常會擴展對象的代碼,以限制修改 base 原型。這樣,完成擴展的對象就可以處理自己的狀態以及需要執行的新功能(僅處理子項需要做的少量更改即可)。
上面這段話是胡說八道,我們真正應該做的是:
- 對修改開放——要做任何更改,我們都要更改 base 原型 / 函數。
- 對擴展封閉——如果要擴展,你就會開始模塊化代碼,不要這么做。確保所有事情都在 base 原型 / 函數中完成。
如果在處理每個場景時都分支,那么上面這兩步帶來的負擔就更重了。所以最后你會看到諸如這樣的代碼:
function makeSound(animal) {
if (animal == "dog") {
return "bark";
} else if (animal == "duck") {
return "quack";
} else if (animal == "cat") {
return "meow";
} else if (animal == "crow") {
return "caw";
} else if (animal == "sheep") {
return "baa";
} else if (animal == "cow") {
return "moo";
} else if (animal == "pig") {
return "oink";
} else if (animal == "horse") {
return "neigh";
} else if (animal == "chicken") {
return "cluck";
} else if (animal == "owl") {
return "twit-twoo"; } else {
/// It has to be a human at this point
return "hi";
}
}
現在,如果你需要更改某些內容,只需添加另一個 if 檢查即可。這和下文列出的多職責原則有關,但核心在于將所有內容都包含在 base 函數 / 原型中。
這樣你就會進入“脆弱基類”反模式。在這種模式下,更改 base 函數 / 原型時,你最后會在調用此函數的其他觸點上出現錯誤。
例如,假設 human 不該再掉進 else 里,而你添加了新的 animal,名為 wolf,你就會引入錯誤(除非你更新了期望 human 被記錄的位置)。
多職責原則
確保你的函數 / 對象有多重職責。
優秀的編程人員常常會將他們的代碼分成多個不同的對象或模塊。但我可搞不清楚這種事情,我記不住它們都負責什么事情。
下面是一個例子:
const godObject = {
handleClicks: function(){},
getUserName: function(){},
handleLogin: function(){},
logTransactionId: function(){},
initialSiteLoad: function(){} };
godObject 負責網站的許多功能。付款、登錄、網站負載、記錄交易 ID 以及網站上的所有點擊功能都包括在其中。太棒了,這樣如果你遇到了錯誤,就會知道它只能出現在這里。確保代碼庫中的每個地方都需要訪問 godObject。讓它做一切工作。
我們在代碼中真正想要的是高耦合(確保系統的各個部分相互依賴)和低內聚(將許多隨機的數據和片段放在一起)。
這種反模式有時被稱為“瑞士軍刀”,因為就算你要的只是一把剪子,但它也可以是指甲銼、鋸子、鑷子、開瓶器,也可以是軟木釘。
我不斷強調的是,要把所有東西放在一起,然后綁定、打包、捆在一起。
總 結
我希望大家看完這篇文章后,就知道軟件究竟應該怎么寫才能盡可能增加調試需求、盡可能把人搞糊涂,并且搞出來最多的技術債。
延伸閱讀:
https://hackernoon.com/introducing-dilos-principles-for-JAVAscript-code-jp1d3w1b