說到物件導向就必須提五個原則,統稱SOLID,它被認為是物件導向的重要概念。這五個原則並不只適用於物件導向,事實上它很像函數式編程的習慣。它的命名很奇怪且容易讓人混淆,所以我會用我自己的翻譯解釋。
Single Responsibility Principle 是「單一職責原則」,認為一個模組應該只有一個職責。模組是將相同功能的函式和類別搜集起來的實體,我們應該根據「職責」將函式裝進一個模組,這樣能提升程式的可維護性。當你想要寫一個網路服務時,你應該要將有關連線的部分拆出來包成一個模組,這樣當程式出錯時更容易找到問題點。類別本身也是一個小模組,因此也適用於這個原則,但它的範圍非常小,所以職責應該也要特別單一,例如管理特定連線狀態。在函數式編程,資料結構沒有職責,畢竟它不會自帶行為,所有關聯方法都是藉由特性系統附加上去的,若要封裝也應該是對這些特性的實作封裝。但資料結構還是應該只有單一語義,複用相同的類型會使程式的可讀性降低。單一職責不代表一定要隱藏實作細節,它只是提供一個組織程式碼的判斷準則。它很常跟「單一功能」(do one thing well)搞混,這是指一個函式應該只做一件事,並把它做到最好。「職責」是指一系列相關的操作,而非一件事,複雜的系統通常需要多個操作才能完整使用。
Open-Closed Principle 是「擴充修改原則」,認為軟體應該是對於擴充開放的,但是對於修改封閉的。當一個公開的函式定義後,就不應該再修改,如果要修改也不應該違反定義函式時文件所描述的特性。這些特性描述是這個函式的承諾,它告訴使用者這個函式在做什麼。如果因為當初設計問題導致承諾本身就是不合理的,應該標示函式為過時並另外定義新的函式取代。如果想要擴充這個函式的功能,則應該另外定義新的函式,並把原本的函式轉接過去,而不應該直接修改原本的函式。也可以透過多型擴充函式,讓原本的函式類型也被涵蓋其中,例如增加可選參數(多載)或是泛型化,但這種方法很容易讓依賴類型推論的情況出問題,因此並不推薦。因為承諾的關係,已經定義好的函式很難再透過修改擴充,因此在定義函式時就應該考慮未來需要擴充的狀況。可以透過泛型化和介面/特性把實作時所用到的方法抽離出來,讓使用者自己決定。這個規則可以應用在任何實體上,例如類型、介面、模組和類別。這個原則將實體的語義與承諾分離出來,強調函式實作應該更著重於實現承諾而非完成業務目標。
Liskov Substitution principle是「子類替換原則」,認為程式中的對象應該是可以在不改變程式正確性的前提下被它的子類所替換的。它描述了子類關係的定義,我們已經在前幾篇文章討論過,在此不再贅述。
Interface-Segregation principle是「介面細分原則」,認為多個特定客戶端介面要好於一個廣泛用途的介面。如果介面包含太多功能,就會使依賴此介面的函式或類別依賴太多無關的方法,使得我們必須為不需要的功能實作方法。透過將功能細分成多個介面,我們可以更容易掌握誰真的依賴於這個方法,從而減少耦合。這對於物件導向來說更重要,因為類別本身就是一個介面,但是當我們增加一些方法給他時,就會讓依賴此類別的函式也增加依賴關係。透過在他們之間定義細分的介面,可以讓函式不會直接受到實作改變的影響,從而起到隔離的作用。
Dependency Inversion Principle是「依賴抽象原則」,認為一個方法應該依賴於抽象而不是一個實例。高階、複雜的操作是由多個低階、特定的操作組成,因此一般來說高階邏輯總是依賴於低階的實作,但這麼做會讓高階的方法很難抽象化。因此應該反過來讓低階的實作依賴於高階的實體,例如透過定義高階的介面讓低階的部分去實作它,而非寫死在高階的方法實作裡。反轉的目的不是為了解決循環依賴的問題,而是為了讓高階的模組更容易抽象化與重用。事實上不是只有高階模組需要抽象化,低階操作不代表它必須寫死成特定實作,具體與抽象的分類和高階與低階的分類並不衝突。解決因依賴造成難以抽象化的方法就是讓實作依賴於抽象,而非直接依賴於實作,具體來說可以參數化依賴的部分讓使用者自己傳入(依賴注入),或是將依賴的特性抽取成介面,並讓具體類別實作這個介面。可以說依賴抽象是一種進階的參數化方法,它把程式碼裡面需要開放的特性作為參數抽取出來,讓程式碼不會直接依賴具體的方法,而是依賴這個參數。
除了子類替換原則,其他的原則基本上都跟物件導向沒有直接關係,它可以應用於任何軟體開發上。這些概念對應於函數式編程的一些習慣用法:OCP擴充修改原則強調承諾的重要性,這對於注重規則的函數式程式語言尤其重要;ISP介面細分原則強調依賴關係的管理,函數式編程常常依據規則建立介面而非依據功能,因此不常遇到這種問題;DIP依賴抽象原則對函數式編程來說只是一種參數化方法,這與高階還是低階、是否反轉依賴關係無關。對於函數式編程,健全的機制往往可以避免生產出不好的程式碼,而非依賴於開發者的素養。或者說當你理解函數式編程的特性,就更容易掌握將業務邏輯轉換成程式設計的邏輯思路的方法。