設計原則:KISS、DRY、LOD 原則
除了了人盡皆知的 SOLID 原則之外,其實還有其他一些有用且很受大家認可的設計原則。本節課就來介紹這些設計原則。主要包括以下三種設計原則:
- KISS 原則;
- DRY 原則;
- LOD 原則。
一、KISS 原則
KISS 原則(Keep It Simple, Stupid)是軟件開發中的重要原則,強調在設計和實現軟件系統時應該保持簡單和直觀,避免過度復雜和不必要的設計。
KISS 原則的英文描述有好幾個版本,比如下面這幾個。
- Keep It Simple and Stupid;
- Keep It Short and Simple;
- Keep It Simple and Straightforward。
不過,仔細看你就會發現,它們要表達的意思其實差不多,翻譯成中文就是:盡量保持簡單。
KISS 原則是保證代碼可讀性和可維護性的重要手段。KISS 原則中的“簡單”并不是以代碼行數來考量的。代碼行數越少并不代表代碼越簡單,我們還要考慮邏輯復雜度、實現難度、代碼的可讀性等。而且,本身就復雜的問題,用復雜的方法解決,并不違背 KISS 原則。除此之外,同樣的代碼,在某個業務場景下滿足 KISS 原則,換一個應用場景可能就不滿足了。
對于如何寫出滿足 KISS 原則的代碼,有下面幾條指導原則:
- 不要使用同事可能不懂的技術來實現代碼
- 不要重復造輪子,要善于使用已經有的工具類庫
- 不要過度優化
下面是一個使用 KISS 原則設計的簡單計算器程序的示例:
package main
import"fmt"
// Calculator 定義簡單的計算器結構
type Calculator struct{}
// Add 方法用于相加兩個數
func (c Calculator) Add(a, b int) int {
return a + b
}
// Subtract 方法用于相減兩個數
func (c Calculator) Subtract(a, b int) int {
return a - b
}
func main() {
calculator := Calculator{}
// 計算 5 + 3
result1 := calculator.Add(5, 3)
fmt.Println("5 + 3 =", result1)
// 計算 8 - 2
result2 := calculator.Subtract(8, 2)
fmt.Println("8 - 2 =", result2)
}
在上述示例中,我們定義了一個簡單的計算器結構 Calculator,包含 Add 和 Subtract 方法用于實現加法和減法操作。通過簡單的設計和實現,這個計算器程序清晰、易懂,符合 KISS 原則的要求。
二、DRY 原則
DRY 原則,全稱為“Don’t Repeat Yourself”,是軟件開發中的重要原則之一,強調避免重復代碼和功能,盡量減少系統中的冗余。DRY 原則的核心思想是任何信息在系統中應該有且僅有一個明確的表達形式,避免多處重復定義相同的信息或邏輯。
你可能會覺得 DRY 原則非常簡單、非常容易應用。只要兩段代碼長得一樣,那就是違反 DRY 原則了。真的是這樣嗎?答案是否定的。這是很多人對這條原則存在的誤解。實際上,重復的代碼不一定違反 DRY 原則,而且有些看似不重復的代碼也有可能違反 DRY 原則。
通常存在三種典型的代碼重復情況,它們分別是:實現邏輯重復、功能語義重復和代碼執行重復。這三種代碼重復,有的看似違反 DRY,實際上并不違反;有的看似不違反,實際上卻違反了。
1. 實現邏輯重復:
type UserAuthenticator struct{}
func (ua *UserAuthenticator) authenticate(username, password string) {
if !ua.isValidUsername(username) {
// ... code block 1
}
if !ua.isValidPassword(username) {
// ... code block 1
}
// ...省略其他代碼...
}
func (ua *UserAuthenticator) isValidUsername(username string) bool {}
func (ua *UserAuthenticator) isValidPassword(password string) bool {}
假設 isValidUserName() 函數和 isValidPassword() 函數代碼重復,看起來明顯違反 DRY 原則。為了移除重復的代碼,我們對上面的代碼做下重構,將 isValidUserName() 函數和 isValidPassword() 函數,合并為一個更通用的函數 isValidUserNameOrPassword()。
經過重構之后,代碼行數減少了,也沒有重復的代碼了,是不是更好了呢?答案是否定的。單從名字上看,我們就能發現,合并之后的 isValidUserNameOrPassword() 函數,負責兩件事情:驗證用戶名和驗證密碼,違反了“單一職責原則”和“接口隔離原則”。
實際上,即便將兩個函數合并成 isValidUserNameOrPassword(),代碼仍然存在問題。因為 isValidUserName() 和 isValidPassword() 兩個函數,雖然從代碼實現邏輯上看起來是重復的,但是從語義上并不重復。所謂“語義不重復”指的是:從功能上來看,這兩個函數干的是完全不重復的兩件事情,一個是校驗用戶名,另一個是校驗密碼。盡管在目前的設計中,兩個校驗邏輯是完全一樣的,但如果按照第二種寫法,將兩個函數的合并,那就會存在潛在的問題。在未來的某一天,如果我們修改了密碼的校驗邏輯,那這個時候,isValidUserName() 和 isValidPassword() 的實現邏輯就會不相同。我們就要把合并后的函數,重新拆成合并前的那兩個函數。
對于包含重復代碼的問題,我們可以通過抽象成更細粒度函數的方式來解決。
2. 功能語義重復:
在同一個項目代碼中有下面兩個函數:isValidIp() 和 checkIfIpValid()。盡管兩個函數的命名不同,實現邏輯不同,但功能是相同的,都是用來判定 IP 地址是否合法的。
func isValidIp(ipAddress string) bool {
// ... 正則表達式判斷
}
func checkIfIpValid(ipAddress string) bool {
// ... 字符串方式判斷
}
在這個例子中,盡管兩段代碼的實現邏輯不重復,但語義重復,也就是功能重復,我們認為它違反了 DRY 原則。我們應該在項目中,統一一種實現思路,所有用到判斷 IP 地址是否合法的地方,都統一調用同一個函數。
3. 代碼執行重復:
type UserService struct {
userRepo UserRepo
}
func (us *UserService) login(email, password string) {
existed := us.userRepo.checkIfUserExisted(email, password)
if !existed {
// ...
}
user := us.userRepo.getUserByEmail(email)
}
type UserRepo struct{}
func (ur *UserRepo) checkIfUserExisted(email, password string) bool {
if !ur.isValidEmail(email) {
// ...
}
}
func (ur *UserRepo) getUserByEmail(email string) User {
if !ur.isValidEmail(email) {
// ...
}
}
上面這段代碼,既沒有邏輯重復,也沒有語義重復,但仍然違反了 DRY 原則。這是因為代碼中存在“執行重復”。這個問題解決起來比較簡單,我們只需要將校驗邏輯從 UserRepo 中移除,統一放到 UserService 中就可以了。
4. 如何提高代碼復用性?
- 減少代碼耦合;
- 滿足單一職責原則;
- 模塊化業務與非業務邏輯分離;
- 通用代碼下沉;
- 繼承、多態、抽象、封裝;
- 應用模板等設計模式。
下面是一個簡單的人員管理系統示例,使用 DRY 原則來確保代碼的清晰和重用性:
package main
import"fmt"
// Person 結構體表示人員信息
type Person struct {
Name string
Age int
}
// PrintPersonInfo 打印人員信息
func PrintPersonInfo(p Person) {
fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
}
func main() {
// 創建兩個人員信息
person1 := Person{Name: "Alice", Age: 30}
person2 := Person{Name: "Bob", Age: 25}
// 打印人員信息
PrintPersonInfo(person1)
PrintPersonInfo(person2)
}
在上述示例中,我們定義了一個 Person 結構體表示人員信息,以及一個 PrintPersonInfo 函數用于打印人員信息。通過將打印人員信息的邏輯封裝在 PrintPersonInfo 函數中,遵循DRY原則,避免重復編寫打印邏輯,提高了代碼的復用性和可維護性。
三、LOD 原則
LOD原則(Law of Demeter),又稱為最少知識原則,旨在降低對象之間的耦合度,減少系統中各部分之間的依賴關系。LOD原則強調一個對象應該對其他對象了解得越少越好,不應直接與陌生對象通信,而通過自己的成員進行操作。
迪米特法則法則強調不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量只依賴必要的接口。迪米特法則是希望減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的類就會比較少。
下面是一個使用LOD原則設計的簡單用戶管理系統示例:
package main
import"fmt"
// UserService 用戶服務,負責用戶管理
type UserService struct{}
// GetUserByID 根據用戶ID獲取用戶信息
func (us UserService) GetUserByID(id int) User {
userRepo := UserRepository{}
return userRepo.FindByID(id)
}
// UserRepository 用戶倉庫,負責用戶數據維護
type UserRepository struct{}
// FindByID 根據用戶ID查詢用戶信息
func (ur UserRepository) FindByID(id int) User {
// 模擬從數據庫中查詢用戶信息
return User{id, "Alice"}
}
// User 用戶結構
type User struct {
ID int
Name string
}
func main() {
userService := UserService{}
user := userService.GetUserByID(1)
fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
}
在上述示例中,我們設計了一個簡單的用戶管理系統,包括 UserService 用戶服務和 UserRepository 用戶倉庫兩個部分。UserService 通過調用 UserRepository 來查詢用戶信息,遵循了LOD原則中只與直接的朋友通信的要求。