介紹
SOLID 是物件導向設計五大原則的縮寫首字母聯想詞,由 Robert C. Martin 提出,他通常被稱為 Uncle Bob。這些原則旨在幫助軟體設計師、架構師、工程師和開發人員建立更具彈性、可維護性和可擴充性的軟體。遵循這些原則,您可以設計出更容易測試、重構、重複使用和擴充的類別。
SOLID 縮寫代表:
S – 單一職責原則
O – 開閉原則
L – 里氏替換原則
I – 介面隔離原則
D – 依賴反轉原則
在本文中,我們將逐一解釋每個原則,以了解它們如何幫助您編寫更好的程式碼。此外,我們還將為每個原則添加程式碼片段,向您展示如何在編碼過程中應用它們,以及在乾淨架構中應該避免什麼。為了演示這些概念,我們將使用由 Kotlin programming 語言,該語言由 JetBrains 和開源貢獻者開發。
單一職責原則
單一職責原則(SRP)是一項軟體設計原則,它指出程式中的每個類別或模組都應該有一個單一且明確定義的職責。這意味著一個類別應該只有一個修改的原因,並且它應該只負責程式功能的單一部分。
請考慮以下程式碼片段,作為如何在 Kotlin:
|
1 2 3 4 5 6 7 8 9 |
data class User( val firstName: String, val lastName: String, val email: String ) |
The data 關鍵字在 Kotlin 中表示該類別是一個資料類別(data class),這意味著它旨在保存資料,並且沒有任何複雜的行為。使用這個資料類別,我們可以建立一個 UserService 類別來管理使用者,如下所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // 在資料庫中建立使用者的程式碼 } fun deleteUser(user: User) { // 從資料庫中刪除使用者的程式碼 } fun updateUser(user: User) { // 在資料庫中更新使用者的程式碼 } fun getUser(id: Int): User { // 從資料庫中檢索使用者的程式碼 } } |
在這個範例中, UserService 類別具有單一職責:在資料庫中管理使用者。每個使用者都由 data class User 先前分享的程式碼表示。 UserService 類別中的所有方法都與此職責相關,因此具有內聚性。這使得該類別更容易理解和維護,因為很明顯該類別中的所有方法都與單一且明確定義的任務相關。
在 UserService 類別中,違反單一職責原則(SRP)的方法會是與該類別的主要職責(即在資料庫中管理使用者)無關的方法。例如,考慮以下 UserService 類別的變體:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // 在資料庫中建立使用者的程式碼 } fun deleteUser(user: User) { // 從資料庫中刪除使用者的程式碼 } fun updateUser(user: User) { // 在資料庫中更新使用者的程式碼 } fun getUser(id: Int): User { // 從資料庫中檢索使用者的程式碼 } fun sendEmail(user: User, subject: String, message: String) { // 向使用者傳送電子郵件的程式碼 } } |
在這個範例中, sendEmail 方法與 的主要職責無關,UserService 類別,該類別的主要職責是在資料庫中管理使用者。此方法負責傳送電子郵件,這與管理使用者是不同的關注點。因此,此方法違反了單一職責原則(SRP),因為它為 UserService 類別引入了第二個需要修改的原因。
為了遵守單一職責原則,最好將傳送電子郵件的功能拆分到一個獨立的類別中,例如 EmailService 類別。這將使 UserService 類別能夠專注於其管理使用者的主要職責,並讓 EmailService 類別專注於其傳送電子郵件的職責。
您應該注意,單一職責原則並非關乎一個類別擁有多少個方法,而是關乎方法的內聚性以及類別內職責的清晰劃分。
開放封閉原則
開放封閉原則(OCP)是一項軟體設計原則,指出軟體實體(如類別、模組或函式)應該對擴充開放,但對修改封閉。這意味著應該能夠在不修改現有程式碼的情況下,為類別或模組新增功能。
考慮前一個範例中的 UserService 類別。假設我們想為 UserService 類別新增一個新功能,允許我們透過電子郵件地址搜尋使用者。實現此功能的一種方法是在 UserService 類別中新增一個方法,如下方醒目顯示的程式碼所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // 在資料庫中建立使用者的程式碼 } fun deleteUser(user: User) { // 從資料庫中刪除使用者的程式碼 } fun updateUser(user: User) { // 在資料庫中更新使用者的程式碼 } fun getUser(id: Int): User { // 從資料庫中檢索使用者的程式碼 } fun searchUsersByEmail(email: String): List<User> { // 透過電子郵件地址搜尋使用者的程式碼 } } |
這種方法可行,但它違反了開放封閉原則,因為我們必須修改現有的 UserService 類別以新增功能。更好的方法是使用繼承或組合來擴充 UserService 類別的功能,而無需修改其程式碼。
為了實現這一點,我們可以建立一個名為 UserSearchService 的新類別,該類別繼承自 UserService 類別並新增電子郵件搜尋功能:
|
1 2 3 4 5 6 7 8 9 |
class UserSearchService(private val userService: UserService) : UserService() { fun searchUsersByEmail(email: String): List<User> { // 透過電子郵件地址搜尋使用者的程式碼 } } |
在這個範例中, UserSearchService 類別對擴充開放,因為它提供了超出 UserService 類別。同時, UserService 類別保持對修改關閉,因為我們不需要修改其程式碼即可新增電子郵件搜尋功能。
里氏替換原則
里氏替換原則(LSP)是一項軟體設計原則,它指出父類別的物件應該要能夠被子類別的物件替換,而不會影響程式的正確性。這意味著子類別應該是其父類別的有效替代品,並且在相同的上下文中使用時,其行為應與父類別相同。
我們將繼續使用 User 和 UserService 類別來進行示範。為了允許 User 類別被繼承,我們在 Kotlin 中使用 open 關鍵字:
|
1 2 3 4 5 6 7 8 9 |
open class User( val firstName: String, val lastName: String, val email: String ) |
以下是原始的 UserService 類別,它使用了上述的 User 資料類別:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // 在資料庫中建立使用者的程式碼 } fun deleteUser(user: User) { // 從資料庫中刪除使用者的程式碼 } fun updateUser(user: User) { // 在資料庫中更新使用者的程式碼 } fun getUser(id: Int): User { // 從資料庫中取得使用者的程式碼 } } |
假設我們想建立一個 User 的子類別,稱為 AdminUser,用來表示具有管理權限的使用者。我們可以這樣做:
|
1 2 3 4 5 6 7 8 9 |
class AdminUser( firstName: String, lastName: String, email: String, ) : User(firstName, lastName, email) |
在這個範例中, AdminUser 類別是 User 類別的有效替代品,因為它的行為與 User 類別相同,並且可以在任何預期使用 User 物件的地方使用。例如,我們可以將 AdminUser 類別與 UserService 類別一起使用,如下所示:
|
1 2 3 4 5 6 7 8 9 |
fun main() { val userService = UserService() val adminUser = AdminUser("John", "Doe", "john.doe@example.com") userService.createUser(adminUser) } |
這段程式碼是正確的,因為 AdminUser 類別是 User 類別的有效替代品,並且可以像 User 物件一樣使用。
值得注意的是,里氏替換原則不僅僅與繼承有關。它是為了確保子類別的物件與父類別的物件具有相同的行為方式,而不管子類別是如何實作的。例如,如果 AdminUser 類別在某些方面的行為與 User 類別不同,這將違反里氏替換原則,因為它將不再是 User 類別的有效替代品。
介面隔離原則
介面隔離原則(ISP)是一項軟體設計原則,它指出用戶端不應被強迫依賴它們不使用的 介面。這意味著,通常最好建立小型且專注的 介面 來做好一件事,而不是建立大型、通用的 介面 來試圖做很多事情。
以下是一個透過重寫 User 和 UserService 類別來應用此原則的範例:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User } class DatabaseUserService : UserService { override fun createUser(user: User) { // 在資料庫中建立使用者的程式碼 } override fun deleteUser(user: User) { // 從資料庫中刪除使用者的程式碼 } override fun updateUser(user: User) { // 在資料庫中更新使用者的程式碼 } override fun getUser(id: Int): User { // 從資料庫中檢索使用者的程式碼 } } |
在這個範例中, UserService 介面定義了四個與在資料庫中管理使用者相關的方法。而 DatabaseUserService 類別實作了此介面,並為這些方法提供了具體的實作。
假設我們想為 UserService 介面新增一個新功能,允許我們透過電子郵件地址搜尋使用者。其中一種方法是在 UserService 介面中新增一個方法:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User fun searchUsersByEmail(email: String): List<User> } |
除非您也在所有實作 UserService 介面的類別中實作此方法,否則您的程式碼將無法執行:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService { override fun createUser(user: User) { // 在資料庫中建立使用者的程式碼 } override fun deleteUser(user: User) { // 從資料庫中刪除使用者的程式碼 } override fun updateUser(user: User) { // 在資料庫中更新使用者的程式碼 } override fun getUser(id: Int): User { // 從資料庫中檢索使用者的程式碼 } override fun searchUsersByEmail(email: String): List<User> { // 透過電子郵件地址搜尋使用者的程式碼 } } |
雖然這種方法可行,但它違反了介面隔離原則,因為它強迫 DatabaseUserService 類別去實作一個( searchUsersByEmail )方法,而該類別可能並不需要或不使用它。
更好的方法是建立一個獨立的 interface 來處理電子郵件搜尋功能:
|
1 2 3 4 5 |
interface UserSearchService { fun searchUsersByEmail(email: String): List<User> } |
現在我們有了獨立、小巧且專注的介面,即 UserService 與 UserSearchService,它們具有單一職責。需要這些介面所有功能的類別可以實現它們,如下面的程式碼片段所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService, UserSearchService { override fun createUser(user: User) { // 在資料庫中建立使用者的程式碼 } override fun deleteUser(user: User) { // 從資料庫中刪除使用者的程式碼 } override fun updateUser(user: User) { // 在資料庫中更新使用者的程式碼 } override fun getUser(id: Int): User { // 從資料庫中檢索使用者的程式碼 } override fun searchUsersByEmail(email: String): List<User> { // 透過電子郵件地址搜尋使用者的程式碼 } } |
這符合介面隔離原則,因為它確保了用戶端(例如 DatabaseUserService 類別)不會被強迫依賴它們不使用的介面。
為了更好地理解這個概念,假設我們有另一個名為 MemoryUserService 的類別,它實現了 UserService 介面,但不需要電子郵件搜尋功能,我們可以這樣編寫程式碼:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserService : UserService { private val users = mutableListOf<User>() override fun createUser(user: User) { users.add(user) } override fun deleteUser(user: User) { users.remove(user) } override fun updateUser(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun getUser(id: Int): User { return users[id] } } |
在這個範例中, MemoryUserService 類別只需要實現定義在 UserService 介面中的方法,而不需要擔心電子郵件搜尋功能。這使得 MemoryUserService 類別能夠專注於其在記憶體中管理使用者的主要職責,而不是被強迫實現無關的功能。
依賴反轉原則
依賴反轉原則(DIP)是一項軟體設計原則,它指出高階模組不應該依賴低階模組,而是兩者都應該依賴抽象。這意味著,通常最好將軟體設計成高階元件不與低階元件的特定實現綁定,而是依賴可以透過不同方式實現的抽象(例如介面或抽象類別)。
讓我們來看一個如何將此原則應用於先前程式碼片段中所使用的 User 和 UserService 類別的範例:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserRepository { fun create(user: User) fun delete(user: User) fun update(user: User) fun get(id: Int): User } class UserService(private val repository: UserRepository) { fun createUser(user: User) { repository.create(user) } fun deleteUser(user: User) { repository.delete(user) } fun updateUser(user: User) { repository.update(user) } fun getUser(id: Int): User { return repository.get(id) } } |
在這個範例中, UserService 類別依賴於一個名為 UserRepository 介面,而不是依賴於使用者儲存庫的具體實作。這使我們能夠以不同的方式實作 UserRepository 介面,例如使用資料庫或記憶體內儲存,而不會影響 UserService 類別。
例如,以下是 UserRepository 介面使用資料庫的實作:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class DatabaseUserRepository: UserRepository { override fun create(user: User) { // 在資料庫中建立使用者的程式碼 } override fun delete(user: User) { // 從資料庫中刪除使用者的程式碼 } override fun update(user: User) { // 在資料庫中更新使用者的程式碼 } override fun get(id: Int): User { // 從資料庫中檢索使用者的程式碼 } } |
以下是 UserRepository 介面的另一個實作,它使用記憶體內儲存:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserRepository : UserRepository { private val users = mutableListOf<User>() override fun create(user: User) { users.add(user) } override fun delete(user: User) { users.remove(user) } override fun update(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun get(id: Int): User { return users[id] } } |
這使得系統更加靈活且易於維護,因為它允許我們在不影響系統其餘部分的狀況下更改儲存庫的實作。這也使得測試 UserService 類別變得更加容易,因為我們可以在測試中模擬 UserRepository 依賴項。
結論
在本文中,我們討論了 SOLID 符合每個原則的程式碼和共享程式碼片段。遵循這些原則可以幫助您設計出更具彈性、可維護性和可擴充性的軟體系統。然而,請務必記住,這些原則是指導方針,而非硬性規定,開發人員需要自行決定何時以及如何在特定專案的背景下應用它們。歡迎查看我們的部落格,閱讀更多深入且最新的文章和 教學課程 關於 雲端運算 與 DevOps、軟體設計與開發、值得關注的技術趨勢等更多內容。
祝您編寫程式愉快!
留言
目前尚無留言。成為第一個留言的人吧。