--- title: '6 大原則' disqus: kyleAlien --- 6 大原則 === ## Overview of Content :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**物件導向設計原則 – 6 大原則(一)**](https://devtechascendancy.com/object-oriented-design-principles_1/)、[**物件導向設計原則 – 6 大原則(二)**](https://devtechascendancy.com/object-oriented-design-principles_2/) ::: [TOC] ## 單一職責 - Single Responsibility Principle * **==單一職責==**:**就一個類而言,應該僅有一個引起它變化的原因** * 例如兩個完全不一樣功能的函數就不應該放在同一類別之中,但是如何單一化責任是透過經驗判斷,並沒有一個標準 :::warning * 由於單一職責是一個較為模糊不清楚的定義,是需要每個人不同的經驗去判斷,所以較容易起爭議 ::: ### BO & Biz 概念區分 * 以往我們接觸的程式都有物件、使用的參雜情況(如下) ```kotlin= interface IBookShop { val name: String val book: String val prize: Int fun buyBook(bookName: String) fun orderBook(bookName: String) fun getBook(phone: String) : String } ``` > 職責不清,如下圖表示 > > ![](https://i.imgur.com/mPxTWO1.png) * 這時就可以 **使用 BO (`Business Object`) 邏輯物件、Biz(`Business Logic`) 邏輯行為,這個依據點作為單一直則的一種區分方式**! 1. **BO (`Business Oject`) 邏輯物件** 把原先的 IBookShop 介面中的「純業務資訊」給抽出,像是書局名稱、書名、價格… 等等封裝 IReaderInfo 介面中,範例程式如下: ```kotlin= interface IReaderInfo { val name: String val book: String val prize: Int } ``` 2. **Biz (`Business Logic`) 邏輯行為** 再把原先的 IBookShop 介面中的「行為動作」給抽出來,像是買書、訂書、取書… 等等行為封裝在 IBookShopAction 介面中,範例程式如下: ```kotlin= interface IBookShopAction { fun buyBook(info: IReaderInfo) fun orderBook(info: IReaderInfo) fun getBook(info: IReaderInfo) : String } ``` > 經過 BO/Biz 整理過後的 UML 如下 > > ![](https://i.imgur.com/mpiBwlY.png) ### 細粒度的考量 * 關於類方法、接口的細粒度,謂何說是 **考量** 呢? 因為若是拆分過細,反而是 **人工增加了系統的複雜度** (生搬硬套只會導致由原則引起的類的劇增),以下舉一個例子範應這個狀況 1. 原本接口的設計,其粒度較大,能夠接收、發送任意數據 ```kotlin= interface ICommunication { fun receiveData(parser: Any) fun sendData(parser: Any) } ``` > ![](https://i.imgur.com/BjU17gb.png) 2. 依據單一職責 parser 的行為應該區分開來,不該由方法中自己實現,這個行為也將接口細粒度變小 ```kotlin= interface IParserData { fun parser(data: Any) } interface IDataTransfer { fun receiveData(parser: IParserData) fun sendData(parser: IParserData) } ``` * 以上這個修改是好的嘛?? **大體上是好的,但它同時增加了使用者使用的複雜度**;下圖中 ^1.^ 組合使用起來就必須實現兩個類,並將其拼湊在一起、^2.^ 直接實現 > ![](https://i.imgur.com/EoV47CD.png) ### 單一職責重點 - 最佳實踐 * 單一職責的特色 * 降低類的複雜度 * 提高程式碼可讀性 * 可維護性高 * 變更的風險降低… 因為職責的清晰劃分,讓更變只會作用在特定類或特定區域(但這不代表這個類不會修改),**所以我們在設計時 最好注意到單一職責,可以降低修改類的影響**! * 最佳實踐方案 * 接口一定作到單一職責;而類的方法設計我們就是盡量~ 達到單一職責 * **對於方法的設計以照一個原則**:這個方法是否能讓人一看見它就知道他是要拿來幹麻的?如果不能一眼看出其功能,代表它不符合單一職責! ```kotlin= interface IUserAccountModify { // 不符合單一職責 fun modifyUserInformation(userName: String, userPhone: String, id: Long) // 符合單一職責 fun changePasswd(userName: String, newPassword: String) } ``` ## 里氏原則 - Liskov Substitution * 里氏原則的全名是 `Liskov Substitution`:**引用基類的地方必須能透明的使用子類的對象,也就是實作的子類別可以隨意替換**,而 **==抽象==(抽象類、界面)** 就是里氏原則的代表 > 簡單來說就是:父類可以出現的地方,替換為某個子類也不會影響其任何功能,並且要「符合父類定義的規範」、「完全在父類的掌控之下」 ```kotlin= // 定義抽象基類 abstract class Shape { abstract fun area(): Double } // 定義子類 - 正方形 class Square(val sideLength: Double) : Shape() { override fun area(): Double { return sideLength * sideLength } } // 定義子類 - 圓形 class Circle(val radius: Double) : Shape() { override fun area(): Double { return Math.PI * radius * radius } } // 函數接受 Shape 對象並計算面積 fun calculateArea(shape: Shape): Double { return shape.area() } fun main() { // 使用正方形 val square = Square(5.0) val squareArea = calculateArea(square) println("正方形的面積:$squareArea") // 使用圓形 val circle = Circle(3.0) val circleArea = calculateArea(circle) println("圓形的面積:$circleArea") } ``` :::success * **里式原則 & 開閉原則** 兩者之間常常是形影不離的,透過里式原則的 **抽象來拓展**,可以達到開閉原則的定義條件(對拓展開放、對修改封閉) * **里氏原則是「語法」(程式面)、「語意」(設計面)相同的推論**,若兩者不符合,則該類不適用於里氏原則 > 通常錯誤都是兩方配合,也就是 誤用語法、誤解語意 造成的 ::: :::warning * 里氏原則不僅限制於類的繼承,也可以用在界面上的繼承(`implements`) > **里氏原則是建立類在界面的「合約」之上!! 為了補足(或是說實現) [規範抽象](https://hackmd.io/o98h12QJQ8io2x7D5Of_qQ?view#%E8%A6%8F%E7%AF%84%E6%A6%82%E8%BF%B0)** ::: ### 繼承規則 - 4 層含意 * 里式替換原則為良好的繼承定義了一個規範,其(繼承)規範包含了 4 層含意 1. **子類必須完全實現父類的方法**!(當然這裡我們不談論中間層的抽象類) * `AbstractPoker` 類:父類宣告抽象方法 ```kotlin= abstract class AbstractPoker { abstract fun material() : String abstract fun playTimes() : Int } ``` * `PaperPoker`、`PlasticPoker` 類:子類必須(一定)要完成父類的方法 ```kotlin= class PaperPoker : AbstractPoker() { override fun material(): String { return "Paper" } override fun playTimes(): Int { return 50 } } class PlasticPoker : AbstractPoker() { override fun material(): String { return "Plastic" } override fun playTimes(): Int { return 500 } } ``` * 作為使用抽象類的開發者,可以宣告父類型作為接收函數,並且可以隨時替換實做,不必擔實做的細節(使用者僅須要了解「抽象界面上給予的承諾」即可) ```kotlin= fun main() { // play 函數不會知道真正的實做 fun play(poker: AbstractPoker) { println("${poker.material()},play times: ${poker.playTimes()}") } play(PaperPoker()) play(PlasticPoker()) } ``` :::danger * 子類實現時要注意「合約」 如果 **子類已經不能完整實現父類的方法,亦或曲解的父類方法的原意(或是不符合業務邏輯),這時就必須斷開父子繼承關係**!改為採用依賴、聚合、組合... 等等關係取代 > 否則其基底就容易歪掉,之後就不容易修正 ::: 2. **子類可以有自己的特性**!這裡的特性就是指子類可以有自己的方法、屬性 ```kotlin= class PlasticPoker : AbstractPoker() { override fun material(): String { return "Plastic" } override fun playTimes(): Int { return 500 } // 子類自身特性 fun waterproof() : Boolean { return true } } ``` :::warning * 但這種寫法也要 **注意 Downcast 的不安全性** Downcast 就是將父類 **強行** 轉換成子類,這個行為可能會倒置 `ClassCastException` 錯誤,因為 **父類沒有子類特性的實做** > 如果發生 ClassCastException 問題,那就違反了里式原則 * 這個描述與前面所說的相同,**都在強調父類(或基類)對象可以被其子類替換而不應該影響程式的正確性** **「透明替換」表示在使用子類對象時,不應該需要知道它是子類,而應該像使用父類一樣。這樣才能確保符合里氏替换原則** ::: 3. **Function ++參數++ 特色:`Overload` 時 子類的參數可以被放大!** 以子類的角度來看 在方法名相同的情況下要重載方法有幾個方式 ^1.^ 不同參數數量(這大家都知道就不說了)、^2.^ **擴大子類的參數範圍**、^3.^ **縮小子類的參數範圍** * **子類擴大(模糊化)的參數範圍**: 如果子類參數擴大,那就 **符合里式原則**;父類出現的地方,子類替換後 **仍在父類控制範圍(這是個重點)** ```kotlin= open class Father { open fun getCollection(map: HashMap<String, String>) : Collection<String> { println("Father work") return map.values } } class Child : Father() { // Overload // 子類放大接收的參數範圍 fun getCollection(map: Map<String, String>) : Collection<String> { println("Child work") return map.values } } ``` * 測試子類擴大參數後是否可被正常訪問? 這裡測試兩種狀況,其實就是父類、子類呼叫同個函數,並觀察是哪個實做類被呼叫到 ```kotlin= fun main() { fun test1() { // 實做類 Father val father = Father() father.getCollection(HashMap<String, String>().apply { put("A", "Apple") }).let { println(it) } } fun test2() { // 實做類 Child val child = Child() child.getCollection(HashMap<String, String>().apply { put("A", "Apple") }).let { println(it) } } test1() test2() } ``` > ![](https://i.imgur.com/C6HLVGP.png) :::success * 從這裡可以看出來,子類完全不會被呼叫到,**證明了子類的擴大參數範圍 (`HashMap` -> `Map`) 無法被指定到(永遠不會)!!** > **保證了父類的規範** 不會被覆蓋 ::: * 子類縮小(更具體)的參數範圍: 縮小參數 **不符合里式原則**;將原先父類的替換為子類,子類 `Overload` 函數後就可能被訪問到 (不在父類控制範圍) ```kotlin= open class Father2 { open fun getCollection(map: Map<String, String>) : Collection<String> { println("Father work") return map.values } } class Child2 : Father2() { // Overload fun getCollection(map: HashMap<String, String>) : Collection<String> { println("Child work") return map.values } } ``` :::danger 曲解了父類的規範,不符合里氏替換原則 ::: * 測試子類縮小參數後是否可被正常訪問:這裡測試兩種狀況,其實就是父類、子類呼叫同個函數,並觀察是哪個實做類被呼叫到 ```kotlin= fun main() { fun test1() { val father = Father2() father.getCollection(HashMap<String, String>().apply { put("A", "Apple") }).let { println(it) } } fun test2() { val child = Child2() child.getCollection(HashMap<String, String>().apply { put("A", "Apple") }).let { println(it) } } test1() test2() } ``` :::info 從結果可以看出來由於子類縮小參數範圍(`Map` -> `HashMap`)導致子類 Overload 的方法不能被覆蓋到,所以最終子類會被訪問! ::: > ![](https://i.imgur.com/ST2NVR9.png) 4. **Function ++返回++ 特色:子類返回的類型可以縮小!** Override 時子類返回的類型可以縮小,也就是可以返回父類定義的子類(**但不能擴大**) > 仍在父類的控制範圍之內就 **沒有違反父類的規範,反而是加強父類的規範!** * 返回類型的繼承關係如下 ```kotlin= // 父類 open class ReturnFather // 子類 class ReturnChild: ReturnFather() ``` * 繼承關係:**Override 時** 子類可以改動返回類型,**子類縮小返回的範圍**,這裡所說的縮小,是指「**特殊化**」(以 UML 的角度來說,父類稱為泛化,子類稱為特殊化) > 這符合里氏原則 ```kotlin= open class Father3 { open fun getSample() : ReturnFather { return ReturnFather() } } class Child3 : Father3() { // 縮小返回的範圍是可以的,仍在父類的掌控範圍之內 override fun getSample(): ReturnChild { return ReturnChild() } } ``` ### 里式原則重點 - 最佳實踐 * **特色**:既然里氏原則的重點是 **抽象**,那我們就要來了解一下抽象「繼承」的優、缺點(這裡特別強調繼承是有原因的,以程式語言來說,繼承是一種達成里式原則的手法) * 抽象「繼承」優點: * 方法、成員的覆用 * 由於是抽象繼承,可拓展性變大 * 細節可由子類決定 * 抽象「繼承」缺點: * **抽象是 ==侵入性==、==靜態==,強迫擁有父類的所有屬性、方法** * 可能造成子類有不需要的方法、成員,**抽象過大時或設計不良時,反而降低了靈活度** * 修改類的代價變大,在修改父類時同時要考慮到是否會影響其他子類 * 最佳實踐方案 * 盡量避免子類的 **個人特性**,一但子類有特性後就可能曲解父類原本設計的意圖,最終會 **導致無法相互替換、不易維護的問題** :::success 當子類有 **個人特性**(或是說與父類不同意圖時),就應該拆分繼承關係! ::: ## 依賴倒置原則 - Dependence Inversion * 依賴倒置原則全名是 `Dependence Inversion Principle`:**高層次模組不依賴於低層次模組的實現細節,反轉模組的依賴關係** 1. 高層模組不一賴於低層模組,**==兩者應該依賴抽象==**(`abstract` or `interface`):實現類與實現類之間不該產生直接關係 * 高層模組:調用者 * 低層模組:實現端 2. 抽象不依賴細節:接口、抽象不依賴實現類 3. 細節應該依賴抽象:實現類應該依賴於接口、抽象 :::success * 類與類之間的關係應該盡可能的依賴各自的抽象,如果互相依賴於細節,兩者之間就會直接產生耦合,導致細節修改後,就需要改動到原來的程式 ::: * DIP 精簡的定義就是 物件導向 的重點 :::warning * 依賴倒置是 6 大原則中最不易實現的,但它是 **實現開閉原則的重要技術** ::: ### 證明 - 依賴倒置 * 要證明依賴倒置原則最常見的方式有兩種:^1.^ **順推驗證**、^2.^ **反證法** 1. **反證法**(RAA):提出 **偽命題**(錯誤命題),然後推導出一個荒漠與已知條件互斥的結論(也就是反向證明) :::info **依賴倒置的 偽命題**:不使用依賴倒置原則也可以減少類之間的耦合關係!來達到低耦合、高穩定、可維護、可拓展性 ::: * 首先寫一個相互依賴細節實做的低層模組,低層模組織間相互依賴(UML 如下) > ![](https://i.imgur.com/PAEAOVW.png) ```kotlin= class Computer constructor(val cpu: CPU) { fun showInfo() { println("CPU: ${cpu.getCore()}") } } class CPU { fun getCore() : Int { return 2 } } ``` * 再創建一個高層模組來使用低層模組 ```kotlin= fun main() { Computer(CPU()).showInfo() } ``` 結果如下 > ![](https://i.imgur.com/I4eao4c.png) * 這時需要替換低層模組 CPU,增大其核心數,來看看是否好增加這個類?可以發現 **被依賴者(CPU)的更動居然要依賴者 (Computer) 來承擔!** ```kotlin= class PowerCPU { fun getCore() : Int { return 8 } } fun main() { Computer(PowerCPU()).showInfo() } ``` > ![](https://i.imgur.com/awSo1bW.png) 2. **順推驗證**(inductive reasoning):根據提出的議題討論,推出和定義相同的結論 :::info **依賴倒置的 討論**:使用依賴倒置原則減少類之間的耦合關係!來達到低耦合、高穩定、可維護、可拓展性 ::: * 透過讓低層模組依賴細節來驗證是否可以有高拓展性 ```kotlin= // 低層模組換成依賴接口 class Computer2 constructor(val cpu: ICPUInfo) { fun showInfo() { println("CPU: ${cpu.getCore()}") } } interface ICPUInfo { fun getCore() : Int } class CPU2 : ICPUInfo { override fun getCore() : Int { return 2 } } class PowerCPU : ICPUInfo { override fun getCore() : Int { return 8 } } ``` * 這樣的關係下,**被依賴者(CPU)的更動 ++不須要++ 依賴者 (Computer) 承擔!** 證明依賴倒置,隱藏了實做細節,提供了最大化的隔離、穩定 :::success * 何為穩定性? 高穩定的設計在周遭環境變化時仍可以作到很少的異動就達成業務需求! ::: ### TDD & 依賴倒置 * TDD 開發模式的技術,就是依賴倒置的最高級應用;先寫好單元測試再進撰寫實現類對於高質量代碼很有幫助! :::danger 1. 有關 Java、Kotlin 單元測試技術,請參考 [**Mockito & Mockk 框架**](https://hackmd.io/eUwLqYBfShmddP54zV-eJg) 2. 接口注入的方式可以參考 [**依賴注入**](https://hackmd.io/kllYxmFSRZiKXt9lrp8KiQ#%E4%BE%9D%E8%B3%B4%E6%B3%A8%E5%85%A5) ::: ### 依賴倒置重點 - 最佳實踐 * 最佳實踐 * 每個類都盡量有接口、抽象類:有了抽象才可以方便置換 * 變量的宣告類型,盡可能的使用抽象 * 如果一個基類已經是一個抽象類,並且它實現某個方法,那這個方法繼承的子類就盡量不要去複寫 > 避免子類曲解了父類方法的原意,造成了不穩定性 * 類應該由抽象產生,而不是具體派生! ```kotlin= // 具體類 class Phone { // 方法只有具體類才有,它定義了細節並暴露了細節 fun prize() : Int { return 1000 } } ``` * **結合里式替換原則使用** * 接口:負責定義公開的屬性、方法,並聲明與其他物件的依賴關係(抽象依賴抽象) * 抽象類:實現共有的業務邏輯,在必要時適當的對父類進行細化 ## 介面隔離原則 - Interface Segregation * 介面隔離原則 全名 `Interface Segregation Principle`,其定義: 1. 客戶端不需要依賴於它不需要的街口,也就是 **讓介面顆粒粒度最小化**,不讓類別依賴他不需要的介面 2. **類與類之間的依賴關係應該建立在最小接口上** * 如果界面過大也會導致使用上的困難(可能強迫了一堆不需要的方法),分散定義多個接口可以防止未來變更的擴散,提高靈活性、可維護性 * 界面隔離的目的是解開系統耦合,讓其更容易的被重構、更改、重新部屬 :::success * **界面隔離 vs. 單一職責** 它們之間的不同主要在於 **==審視角度== 的不同** 1. 單一職責重點是業務上的劃分,其可能有許多方法在一個類、接口中 2. 接口隔離則是接口中的方法盡可能的少,有幾個模塊就要有幾個接口 ::: ### 界面最小化 * 這裡定義了一個 dyson 空氣清淨機界面,它的基本功能就是風扇、空氣清淨並使用,範例程式如下 1. 空氣清淨機界面功能總和 ```kotlin= interface IFan { fun airVolume() : Int fun cleanAir() : Boolean } ``` 2. dyson 牌空氣清淨實現類 ```kotlin= class Dyson : IFan { override fun airVolume(): Int { return 3 } override fun cleanAir(): Boolean { return true } } ``` 3. 建立一個假的使用者(高層模組),他使用 IFan 界面(依賴低層模組) ```kotlin= class User(private val fan: IFan) { fun useFan() { println("Air volume(${fan.airVolume()}, clean air(${fan.cleanAir()}))") } } ``` 使用者依賴一整塊完整功能的接口,其 UML 如下 > ![](https://i.imgur.com/qFZnQrc.png) * 以上這看似一個好的設計(**符合單一職責**),但是存在接口上設計的些許紕漏,它的界面不夠細緻,這導致了使用者依賴了過多方法 > 假設使用者需要一個不用空氣清淨的單純風扇?那就必須修改實做 Dyson 類 1. **最小化界面,將最每個方法拆分** ```kotlin= interface IAirFan { fun airVolume() : Int } interface ICleanAir { fun cleanAir() : Boolean } ``` 2. 依照業務功能,針對不同產品實做不同接口 ```kotlin= class Dyson2 : IAirFan, ICleanAir { override fun airVolume(): Int { return 3 } override fun cleanAir(): Boolean { return true } } class OnlyFan : IAirFan { override fun airVolume(): Int { return 5 } } ``` 3. 修改使用者,讓使用者依賴最小接口 ```kotlin= class User2(private val fan: IAirFan, private val clean : ICleanAir) { fun useFan() { println("Air volume(${fan.airVolume()}, clean air(${clean.cleanAir()}))") } } ``` **這樣最小化接口,並可以保持接口的穩定度,也方便之後重構(影響會較小)**;其 UML 概念圖如下 > ![](https://i.imgur.com/C85TVln.png) ### 界面方法 - 合適大小 * **界面最小化的原則**: 小要有一定的限度,重點是不能違反單一職責!**如果 界面隔離、單一職責 衝突**,則以單一職責為主,因為 **單一職責才是業務的重點**!(首先我們必須先滿足業務需求) * **界面的高內聚**: 接口是對外的承諾,承諾越少其風險越低,修改時的代價就相對的低 :::info * 何謂高內聚? 高內聚就是提高界面、類或是模塊處理業務的能力,減少對外的耦合依賴 > 通常方法越多其依賴耦合越大 ::: * **訂製化接口**: 為了單一個體需求客制接口服務 * **界面設計的限制**: 界面最小化的靈活同時也帶來了更多個界面、檔案,複雜化並增加開發的代價、維護成本變高;但它並沒有不好,而是我們必須通過經驗來判斷 ### 界面隔離重點 - 最佳實踐 * 其實不只是界面、更甚是類也應該有這個觀念,而最小化的是沒有準則的,但我們可以根據一些條件來考量 1. 一個接口只服務一個子模塊、或是業務邏輯 2. 把業務邏輯設置到接口中,並時常回顧思考 3. 若接口已經複雜化 * 尚未高頻率使用:重構 * 高頻率使用、已到 release 環境:使用 Adapter 隔離接口,重新打造乾淨環境 4. 了解環境、**拒絕盲從**!深入了解業務邏輯,最好的界面就出自於你手~ ## 迪米特原則 - Low of Demeter * 迪米特原則 `Low of Demeter` 又稱為最少知識原則,**一個對象為了減低耦合性,應該對其他類的依賴越少越好** > 每個對象都一定會與其他對象產生耦合關係,這耦合關係可能是聚合、依賴、組合、關聯...等等 :::success * 迪米特原則 還有另外一個英文描述 `only talk to your immediate friends` ::: ### 最少知識 - 類的關係 1. **不出現關係三角關係(或是更多)**: * 首先先看一個有三角關係的類圖(分為下載管理、下載行為、檔案資訊),其 UML 類關係如下,可以看出三角關係 ```kotlin= class FileTask constructor(private val action: DownloadAction) { fun getDownloadAction() : DownloadAction { return action } } class DownloadAction constructor(private val url: String) { fun startDownload() { println("Download $url") } } class DownloadManager { fun start() { val action = DownloadAction("http//:www.test.123") val task = FileTask(action) task.getDownloadAction().startDownload() } } ``` > ![](https://i.imgur.com/A3axYrd.png) * 依照最少知識原則,**讓一個類認識更少的類** ```kotlin= class FileTask2 constructor(private val url: String) { fun start() { DownloadAction2(url).startDownload() } } class DownloadAction2 constructor(private val url: String) { fun startDownload() { println("Download $url") } } class DownloadManager2 { fun start() { FileTask2("http//:www.test.123").start() } } ``` > ![](https://i.imgur.com/Wa4KEoe.png) 2. **類與類之間的關係是建立在類之間,不是方法之間**,這是啥意思? 也就是說一個類不要通過一個方法隱性引入另一個類 (Link),這會使關係過於複雜,看以下範例 > 當然這是說返回都是不同類型時就會過於複雜,如果返回的是自身(this)那就不會有這種問題 ```kotlin= class A { fun getB() : B = B() } class B { fun getC() : C = C() } class C { fun showInfo() { println("A->B->C") } } fun main() { // 極為不推薦這種做法 A().getB().getC().showInfo(); } ``` ### 暴露細節 - 使用者代價 * 設計一個類(或接口)時我們為了分清方法責任,有時會把方法拆分得很細,然而對於使用者來說這樣好嗎?這個答案是不一定,但可以幾個準則 1. **業務邏輯上是否需要拆分這麽細**? > 拆分的過細是否符合高內聚的設計概念 2. **使用者使用這些方法是否代價過大**(承擔了過多個細節)? > 是否需要主動不斷調用下一個方法才能達到業務邏輯 * 範例程式(這裡不關心依賴倒置問題,專注看迪米特原則): 1. **對外暴露較多方法**:使用者經過層層調調用才能達到某個業務目的,代價較大 ```kotlin= class HttpTask { var url: String? = null } class DownloadPool constructor(private val task: HttpTask) { fun start() : Boolean { println("Start task: ${task.url}") return task.url != null } } class DownloadController { private val task = HttpTask() private var pool: DownloadPool = DownloadPool(task) private var isSuccess: Boolean = false fun setUrl(url: String) { task.url = url } fun getUrl() : String? { return task.url } fun startDownload() { isSuccess = pool.start() } fun getDownloadResult() : Boolean { return isSuccess } } fun main() { // 使用者調用不易,需要用很多方法 DownloadController().apply { if (getUrl() == null) { throw Exception("Already set url.") } setUrl("http://www.123") startDownload() getDownloadResult().also { println("is download success? $it") } } } ``` 2. **減少對外暴露的方法**:善用私有方法(或保護方法),來實現高內聚類;使用者調用起來極其簡單,很快速的可以達到業務邏輯 ```kotlin= class HttpTask2 { var url: String? = null } class DownloadPool2 constructor(private val task: HttpTask2) { fun start() : Boolean { println("Start task: ${task.url}") return task.url != null } } class DownloadController2 { private val task = HttpTask2() private var pool: DownloadPool2 = DownloadPool2(task) private fun setUrl(url: String) { task.url = url } private fun getUrl() : String? { return task.url } fun startDownload() : Boolean { if (getUrl() == null) { throw Exception("Already set url.") } setUrl("http://www.123") pool.start() return pool.start() } } fun main() { // 使用者簡單調用,即可達成業務目標 DownloadController2().startDownload() } ``` :::info * 設計時可以在反覆衡量是否要減少類的方法,因為對外開出的方法越多,修改時涉及的面積就越大 ::: ### 方法要放在哪個類? * 如果遇到一個方法可以放在 A 類,也可以放在 B 類(或其他類),那我們可以用一個簡單原則來歸納該方法是否可以放在某個類中 1. **該方法是否會增加類之間的關係**?(是否邏輯變複雜) 2. 該方法是否會產生不良的負面影響(可能是 **改變類的責任**、或是 **不符合界面隔離**... 等等參考) ### 迪米特重點 - 最佳實踐 * 迪米特原則的重點在於: * 解耦類與類之間的關係,將其改為弱耦合關係 :::danger * 缺點也很明顯:出現大量的中間類、攜帶過多的參數、複雜度變高;要反覆權衡才能達到最好的平衡 ::: * **最佳實踐** * 如果一個類經過兩次調用(最多兩次)才能達到其效果,就要考慮重構了 > 轉跳越多層其複雜度越高 ## 開閉原則 - Open Close Principle * **對 ++新增開放++ 對 ++修改關閉++,透過繼承升級或擴充程式**,也就是說一個軟體實體類應該透過 **擴展** 來實現、擁抱商業邏輯的改變 > 開放範圍 : 對於類別、模組、函數等等的擴充開放 > > 抽象在程式中是指抽象(`abstruct`)、接口(`interface`) * 開閉原則是維運的重點 維運是對於沒有問題的類不會進行修改(透過拓展滿足業務),保證了原有類、方法一定能執行,不會因為修改而導致錯誤; 然而假設你是處於重構階段,這個原則則是讓你考量未來、重構設計的重點 :::success * **開閉原則是一個抽象概念,指導的 5 個規則都是開閉原則的具體型態(指到我們設計的方法、工具)!** ::: ### 變化模塊 - 範例 * 會變化的三種類型(方向) 1. **邏輯變化** 不涉及任何模塊,但純改變算法、邏輯概念 > 修改完後記得去檢查所有依賴類,是否都符合新修改的邏輯(最好就是有單元測試保護) 2. **低層模組(子模組)變化** 就上面例子中的拓展類 `MovieLoader` 就是低層模組,它實現了新的業務邏輯來符合需求,最終高層模組再修改 > 下面會舉個例子 :::warning * 高層模組的修改是必然,它的修改並不算在開閉原則之外(要符合業務邏輯就一定要修改到應用的高層邏輯) ::: 3. **視圖變化** 就是 UI 界面設計的變化,它可以能間接的影響(考驗)到你程式設計是否可以 Handle 住 * 開閉原則建議我們以擴充(變化模塊)的方式來達成業務需求,而不是透過修改已有的程式來達成業務需求,那我們就要思考一下 **是哪個區塊的變化才算達成開閉原則** ? > 以下舉個例子來說明 1. **抽象類定義**:這個抽象類是對外的一個 **契約**,透過這個契約我們可以拓展不同的實做,來達到不同的業務邏輯 ```kotlin= abstract class BaseLoader { protected var cache = LibraryCache() abstract fun download(name: String) abstract fun read(index: Int): String } ``` 2. **低層模組(實做抽象)**:實現基礎業務邏輯的地方 ```kotlin= // 低層模組 class BookLoader : BaseLoader() { override fun download(name: String) { cache.setCache(name).also { println("download book") } } override fun read(index: Int): String { return cache.getCache(index).also { println("read book = $it") } } } ``` 3. **高層模組(應用)**:應用並組合一到多個低層模組,最終實現一個完整的業務邏輯 ```kotlin= fun main() { var loader : BaseLoader = BookLoader() loader.apply { arrayOf("平行世界", "奇異點", "量子力學", "粒子加速器", "5G 世界").forEachIndexed { index, book -> download(book) read(index) } } } ``` > UML 如下圖 > > ![](https://i.imgur.com/67bHdHo.png) * 從上可以看出 **`BaseLoader` 是一個契約**,我們可以透過抽象的契約來拓展不同的業務邏輯,如下 ```kotlin= // 新拓展的業務邏輯 class MovieLoader : BaseLoader() { override fun download(name: String) { cache.setCache(name).also { println("Use Live TV") println("download movie") } } override fun read(index: Int): String { return cache.getCache(index).also { println("Use Live TV") println("watch movie = $it") } } } // 高層模組的修改 fun main() { var loader : BaseLoader = BookLoader() loader = MovieLoader() loader.apply { arrayOf("大亨小傳", "雞不可失", "葉問 4", "白頭山", "阿甘正傳").forEachIndexed { index, movie -> download(movie) read(index) } } } ``` ### 開閉原則 - 優點 1. **對於單元測試的友善** 基上我們在做單元測試時,一個方法很容易超過 3 個以上的測試,正常業務邏輯、邊界條件測試、異常測試(這裡我們撇除測試的必要性,假設做完整的單元測試) :::info * 透過 測試框架 可以快速定義抽象界面,讓我們快速製作假物件進行測試! ::: 2. **提高覆用性** 首先談談謂何要覆用? 簡單來說就是避免寫重複的代碼邏輯 而所有的商業邏輯都是從原子(最小)邏輯組合而來,其粒度越小,未來我們越容易覆用;而開閉原則可以把邏輯封裝在抽象,並向上提供界面,這大大提高了可覆用性(也就是說同樣的方法不用寫了再寫) > 當然這裡不討論粒度大小造成的複雜性 3. **可維護性** 維護性,就要提到維運人員,他們的職責多是應用已有的框架來達成業務邏輯而不是創造,如果你的 **程式沒有可拓展性,那對應的就沒有可維護性**! 4. **物件導向開發** 在設計之初把可能的變化因素留下抽象界面,這能讓未來產品需求變動時,更快的適應新需求! ### 開閉原則的核心 * 開閉原則是一種概念(或是說口號),並有沒有像其它 5 點一樣就具體的定義,但我們可以透過以下幾個方向來了解到開閉原則的核心概念 1. **抽象約束:對擴展開放的首要前提,就是 ++抽象對於類的約束++!** :::info * 抽象約束包括三個子概念:^1.^ 通過界面 或 抽象約束類、^2.^ 參數類型、返回類型盡量使用界面 或 抽象、^3.^ 抽象層盡量不要隨意修改 ::: 抽象、界面(契約)是對物件的通用描述,它並不代表具體的實現邏輯;我們 **可以透過抽向來達到對於實體類的約束!** ```kotlin= // 透過抽象來定義 通用描述(功能) interface IBookShop { fun getBook(name: String) : Boolean fun getPrize() : Int } // 透過抽向來限制實體類 class MyBookShop : IBookShop { override fun getBook(name: String): Boolean { TODO() } override fun getPrize(): Int { TODO() } // 如果透過抽象類訪問對象,這個方法就無法被訪問 fun showBookInformation(name: String) { // do something } } ``` 外部取用時,也就只能取用到抽象定義的方法!無法使用到實體類的方法 ```kotlin= fun main() { val shop : IBookShop = MyBookShop() println("Prize: ${shop.getPrize()}, Name: ${shop.getBook("123 木頭人")}") // 由於被限制範圍,所以無法訪問實做類的方法 shop.showBookInformation("123 木頭人") } ``` 2. **使用元數據 (meta data) 對控制模塊** 元數據 (meta data) 感覺很難?但其實它也是一個編程,他是對已有的程式、環境進行拓展編譯,就像是 Java 的反射機制 > 簡單來說就是透過 時實(Real time)數據 來操控程式,而不是事先先編譯好的程式 :::success 這種操縱方式到達高級的境界就是 **控制反轉(Inversion of Control)** ::: 3. **規範界面** 對於團隊開發來講,你保證了一個功能界面,對於其他成員來說,使用起來就只需要知道界面即可,這樣合作起來就會相對高效 4. **界面封裝變化** 就如同迪米特法則一樣,使用者只須知道這個界面提供哪些功能,而不並擔憂這個界面的實現狀況 詳細來說封裝變化有兩個要點要注意 * 將相同變化封裝到同一界面 * 將不同變化封裝到不同界面 :::info 我們所知的 23 個程式設計模式都是從 **不同的角度** 對變化進行一定程度的封裝! ::: ### 開閉原則重點 - 最佳實踐 * 首先我們要知道,**開閉原則是其他 5 大原則的核心** > ![](https://i.imgur.com/SxwY9BF.png) * 最佳實踐 * 6 大原則概念 6 大原則可以協助我們快速適應產品的變化(前提是類做到了高內聚、低耦合否則很難達到好的設計),**但請不要侷限於這 6 大原則** * 規範界面 盡量讓自己的界面穩定避免不斷修改(不然別人使用起來也會很難用) * 預知變化 架構師設計一套系統需要符合當前業務邏輯,同時也要預測未來的拓展可能性(當然這不是 100% 能辦到的),才可以擁抱改變 ## 更多的物件導向設計 物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)! :::info * [**設計建模 2 大概念- UML 分類、使用**](https://devtechascendancy.com/introduction-to-uml-and-diagrams/) * [**物件導向設計原則 – 6 大原則(一)**](https://devtechascendancy.com/object-oriented-design-principles_1/) * [**物件導向設計原則 – 6 大原則(二)**](https://devtechascendancy.com/object-oriented-design-principles_2/) ::: ### 創建模式 - Creation Patterns * [**創建模式 PK**](https://devtechascendancy.com/pk-design-patterns-factory-builder-best/) * **創建模式 - `Creation Patterns`**: 創建模式用於「**物件的創建**」,它關注於如何更靈活、更有效地創建對象。這些模式可以隱藏創建對象的細節,並提供創建對象的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結 :::success * [**Singleton 單例模式 | 解說實現 | Android Framework Context Service**](https://devtechascendancy.com/object-oriented_design_singleton/) * [**Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer**](https://devtechascendancy.com/object-oriented_design_abstract-factory/) * [**Factory 工廠方法模式 | 解說實現 | Java 集合設計**](https://devtechascendancy.com/object-oriented_design_factory_framework/) * [**Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗**](https://devtechascendancy.com/object-oriented_design_builder_dialog/) * [**Clone 原型模式 | 解說實現 | Android Framework Intent**](https://devtechascendancy.com/object-oriented_design_clone_framework/) * [**Object Pool 設計模式 | 實現與解說 | 利用 JVM**](https://devtechascendancy.com/object-oriented_design_object-pool/) * [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/) ::: ### 行為模式 - Behavioral Patterns * [**行為模式 PK**](https://devtechascendancy.com/pk-design-patterns-cmd-strat-state-obs-chain/) * **行為模式 - `Behavioral Patterns`**: 行為模式關注物件之間的「**通信**」和「**職責分配**」。它們描述了一系列對象如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結 :::warning * [**Stragety 策略模式 | 解說實現 | Android Framework 動畫**](https://devtechascendancy.com/object-oriented_design_stragety_framework/) * [**Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService**](https://devtechascendancy.com/object-oriented_design_interpreter_framework/) * [**Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞**](https://devtechascendancy.com/object-oriented_design_chain_framework/) * [**State 狀態模式 | 實現解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_state/) * [**Specification 規格模式 | 解說實現 | Query 語句實做**](https://devtechascendancy.com/object-oriented_design_specification-query/) * [**Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_command_servant/) * [**Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存**](https://devtechascendancy.com/object-oriented_design_memo_framework/) * [**Visitor 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_visitor_dispatch/) * [**Template 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_template/) * [**Mediator 模式設計 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_mediator/) * [**Composite 組合模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_composite/) ::: ### 結構模式 - Structural Patterns * [**結構模式 PK**](https://devtechascendancy.com/pk-design-patterns-proxy-decorate-adapter/) * **結構模式 - `Structural Patterns`**: 結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結 :::danger * [**Bridge 橋接模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_bridge/) * [**Decorate 裝飾模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_decorate/) * [**Proxy 代理模式 | 解說實現 | 分析動態代理**](https://devtechascendancy.com/object-oriented_design_proxy_dynamic-proxy/) * [**Iterator 迭代設計 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_iterator/) * [**Facade 外觀、門面模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_facade/) * [**Adapter 設計模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_adapter/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Java 設計模式`