# SOLID 原則 在 Uncle Bob 書中 <*Clean Architechture*> 中有提到 SOLID 原則,這些原則是物件導向設計的一大設計準則,當軟體有依循這個原則時,會比沒有遵循的來得彈性更高,同時若有仔細體會這幾個原則,會發現 Clean Architechture 的種種因素都是源自於此。 ## Single Responsibility Principle Single Responsibility Principle(SRP) 單一職責原則,在看了 FB 上看了某個工程師的一個分享有關於寫測試的重要性中,講者說了一個挺有趣的現象,他說問面試者 SOLID 原則是什麼後,90% 的面試者可以講得出單一職責原則,後面的 OLID 都答不太出來。 Anyway. 何謂單一職責原則,相信某些人應該會說:「就是一個類別或一個函數只負責一件事情呀」,但是如果你仔細想想,你會發現一個問題 > 一件事情是指什麼? 每個人所定義的一件事情可能有大有小,甚至是不一樣。假設你開發一個購物網站,需要管理資料庫產品資料,所以你在你的後端程式增加個一個 ProductRepository 類別負責管理。乍一聽還不錯,結果你的夥伴跟你說,「我覺得你沒有符合單一職責原則,應該要一個類別負責所有東西的讀,一個負責所有東西的寫...」,~~你聽了一定想打他一巴掌說這是什麼狗屎玩意~~,但你想想他說的貌似也符合所謂的「一件事情」。 為了解決這種定義問題,比較精準的定義其實是說「一個類別只能有被改變的原因」,而 Uncle Bob 在書中重新解釋為「一個類別應該只能服務於一個角色 (actor)」。 從 Uncle Bob 在書中所說,單一職責原則是針對類別或方法的,實際上再更上升到 component 和 architecture 時會有其他的原則(如果我沒記錯架構層級那塊需要劃分 boundary,這塊就是乾淨架構中的另一個議題了,我就不在這篇討論) 總之,單一職責原則我認為是 SOLID 裡面最難掌握且最不好精通的一個原則。經驗不足的人粒度切分太細,導致整體類別或方法過多,形成一種過度工程化 (over-enginnering);粒度切分太大會造成本身可能就沒有達成單一職責原則,導致在修改的時候牽一髮而動全身。 待補 ## Open Closed Principle Open Closed Principle(OCP)開放封閉原則,一句話帶過就是「你的程式應該要對於原本的修改封閉,對擴展開放」。 > You should be able to extend the behavior of a system without having to modify that system. 意思就是指說當你今天設計好了一套軟體,這時客戶來向你靠夭說他們要加一些功能,當你的程式符合開放封閉原則時,這時你理論上是完全不需要去更改你以前寫過的 code,而是在既有的架構下再去新增。 然而理想很豐滿,現實很骨感。 我在水球軟體學院有聽了水球潘大神的分享,實際上在開發時不免需要動到一點以前寫過的東西,所以說 OCP 是軟體架構學的終極目標,這個原則不太可能完全達到,不過至少我們可以利用一些設計模式在小範圍的地方達到 OCP,例如策略模式、責任鍊模式都可以在既有的 code 下去不斷新增東西。 而我在看 Uncle Bob 的演講時,我覺得他也說了一句非常有道理的一句話,同時在乾淨架構的分層中可以看出意義的 > If something change a lot, it should be a plugin; if something doesn't really change often, then it should be plugged into 當一個東西變動很大時,他應該要是插件,而當一個東西不常變動時,那他應該要是被插件所附著的一方。看看 Google chrome 插件,看看 Vscode extension,這些都是很不錯 OCP 例子。 ## Liskov Substitution Principle Liskov Substitution Principle(LSP)里氏替換原則,這個類別要理解其實也是相對容易。 > Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. 若你有寫過物件導向程式的話,有一個很常見的例子就是以下: 若我有一個鳥的抽象介面(類別) ```go= type interface Bird { fly() error } ``` 這時很自然的我會新增烏鴉、麻雀等各種鳥類 ```go= type struct Crow{} func (c Crow*) fly() error { fmt.Println("烏鴉飛"); return nil } type struct Sparrow{} func (s Sparrow*) fly() error { fmt.Println("麻雀飛"); return nil } ``` 突然來了一個企鵝,你赫然發覺企鵝是鳥但他不會飛,於是你就給他丟了個錯誤 ```go= type struct Penguin {} func (p Penguin*) fly() error { return errors.Error("我是企鵝我不會飛") } ``` 這其實就違反了里氏替換原則,別人用了你的 code,其他鳥都飛得好好的,這時突然來了個企鵝,其他人還要幫你想企鵝的 special case 怎麼處理。這和上面所說的「當多個子類別繼承父類別時,子類別之間要是能夠抽換的」明顯是有衝突的。 而我個人認為這個原則和待會要介紹的介面隔離原則是相輔相成的。 ~~而館長其實也對於里式替換原則講了很有意思的描述~~ > ~~你們一定要... 傳承我的精神...RRR~~ ## Interface Segregation Principle Interface Segregation Principle(ISP)介面分離原則,這個原則說難不難,說簡單也不簡單吧。 > No code should be forced to depend on methods it does not use. 介面分離原則其實和里氏替換原則稍微有一點點相同的地方,其實我上面舉的例子同樣也違反了介面分離原則,企鵝根本不需要 fly 這個 function,然而你卻強迫他必須要實作這個功能,於是只好直接回傳錯誤。 也就是說當一個介面(或類別)提供了一些方法,然而依賴於這個介面的類別根本不會(或不該)用到某些特定的方法,這其實就違反了介面分離原則。這邊我引用 [YC 在 Medium 上的例子](https://medium.com/%E7%A8%8B%E5%BC%8F%E6%84%9B%E5%A5%BD%E8%80%85/%E4%BD%BF%E4%BA%BA%E7%98%8B%E7%8B%82%E7%9A%84-solid-%E5%8E%9F%E5%89%87-%E4%BB%8B%E9%9D%A2%E9%9A%94%E9%9B%A2%E5%8E%9F%E5%89%87-interface-segregation-principle-50f54473c79e)。 ```csharp= class Car { public function void openEngineMode() { /*...*/ } public function void repairWheel() { /*...*/ } public function void startEngine() { /*...*/ } public function void move() { /*...*/ } } class Driver { Car myCar = new Car(); myCar.startEngine(); myCar.move(); myCar.openEngineMode(); // 為什麼我什麼都不會就可以開啟工程模式呢? } class Mechanic { Car clientCar = new Car(); clientCar.repairWheel(); clientCar.openEngineMode(); } ``` 一個普通的駕駛理論上是不能啟動工程模式的,但在上面的例子中卻給普通駕駛提供了這個功能。Car 這個類別給了普通駕駛他根本不該用到的功能或方法,而解決的辦法就是使用介面的方式來限制普通駕駛和技工的方法使用權限。 ```csharp= interface DailyUsage { public function void startEngine(); public function void move(); } interface RepairUsage { public function void openEngineMode(); public function void repairWheel(); } class Car implement DailyUsage, RepairUsage { public function void openEngineMode() { /*...*/ } public function void repairWheel() { /*...*/ } public function void startEngine() { /*...*/ } public function void move() { /*...*/ } } class Driver { DailyUsage myCar = new Car(); myCar.startEngine(); myCar.move(); } class Mechanic { RepairUsage clientCar = new Car(); clientCar.repairWheel(); clientCar.openEngineMode(); } ``` ## Dependency Inversion Principle > 1. High-level modules should not depend on low-level modules. Both should depend on abstractions. > 2. Abstractions should not depend on details. Details should depend on abstractions. Dependency Inversion Principle(DIP) 依賴翻轉原則,這一個原則我認為要實際實作過一些專案後才比較容易體會,同時你在看 clean architechture 中你會讀到書中有給一個限制為,依賴的方向必須是單一方向由外向內指。然而在實際的情況中某些組件會在比較外層的 layer,而內部 layer 會需要依賴那個外層的組件,此時會發現不符合乾淨架構中的限制,這時就要引進 DIP 的概念了。 這邊我舉一個我理解比較深刻的一個例子,同時我也是因為那個例子才對依賴翻轉原則才有更深的理解。 一般來說我們在開發後端時,若參照乾淨架構的圖來看(下圖),通常會在 use case 裡面使用到 repository,而 repository 負責跟資料庫做溝通,把 entities 傳給 use cases 用,這邊你如果思考一下,會發現 repository 處在 gateway 層(綠色層),因為他是資料庫和應用程式之間的轉接器。 ![](https://i.imgur.com/UsTDemF.png) 有沒有覺得哪裡怪怪的?use cases 需要依賴 gateway 層的 layer,這不是違反了乾淨架構的限制嗎(依賴方向要向內指)?這時就需要使用依賴翻轉的技術了 ![](https://i.imgur.com/MtOuB6H.png) 我們在 use cases 層中放一個 UserRepo 的介面,讓我們的 use cases 中依賴這個介面,並且在 adaptor layer 中宣告類別 UserRepoImpl 使其實作 UserRepo 這個抽象介面,如此一來我們藉由這個介面就達到了依賴翻轉原則,重新讓外部向內依賴,並且也符合了定義 1 和 2。 另外說到高階類別不依賴於低階實體類別時,可以看我畫的那張圖,UserController 直接依賴實體的 UserService 這樣其實就不符合了 DIP 的定義,因此要怎麼做讀者可以自己想想看喔。 <!-- - SOLID原則 - 單一職責原則 - 開放封閉原則 - 里氏替換原則 - 介面分離原則 - 依賴翻轉原則 - 乾淨架構 - 分層介紹 - Entities(domain) layer - Use cases layer - Application(Adaptor) layer - External devices(DB, framework, view) layer - 限制 - 依賴方向由外向內單向 - 優點 - 獨立性高、彈性好 - 測試性高,尤其單元測試 - Case study - Use - 不適和使用時機 - 介紹 context 概念並且察覺 forces - 從程式碼行數來看 小於 5000 行作者認為沒有必要 - 從 context 來看,新創產業可能不適和 (domain 變動大) - The only way to get fast is to go well -->