筆者在最近幾個月陸續看了一些與設計模式、軟體重構有關的書籍:
補充:
設計重構這本書主要是在談軟體開發的技術債、介紹常見的軟體設計臭味與可能的解決辦法,雖然它介紹的臭味比較容易出現在物件導向的程式語言身上,不過我們還是可以吸收其中的精華並且應用到日常的實戰中。
上面提到的書籍有些談論軟體臭味、有些談論如何在一開始就給出更好的設計或是使用更好的方法進行抽象,但這些文獻想要表達的重點不外乎就是:
而學習設計模式可以幫助我們一定程度上的避免寫出有問題的程式碼,不管是多執行緒程式的設計或是模組之間的依賴關係,我們幾乎都能以自己的情境去找到最合適的設計模式。
如果學過作業系統或是並行程式設計,你也許會知道當使用 mutex lock 時,其實會導致沒有取得 lock 資源的 process/thread 進入休眠,如果要開發一個高效能的網路程式,我們需要避免在程式會頻繁經過的 critical section 使用 lock,舉例來說:
package main
import (
"fmt"
"sync"
)
const (
MSG_A = iota + 1
MSG_B
MSG_C
)
var MLock sync.Mutex
type Msg struct {
Type int
}
func MsgDispatcher(msg *Msg) {
switch msg.Type {
case MSG_A:
go HandleMsgA(msg)
case MSG_B:
go HandleMsgB(msg)
case MSG_C:
go HandleMsgC(msg)
default:
fmt.Println("Unknown msg type")
}
}
func HandleMsgA(msg *Msg) {
MLock.Lock()
NewTransaction()
fmt.Println("Handle MSG A")
MLock.Unlock()
}
func HandleMsgB(msg *Msg) {
MLock.Lock()
NewTransaction()
fmt.Println("Handle MSG B")
MLock.Unlock()
}
func HandleMsgC(msg *Msg) {
MLock.Lock()
NewTransaction()
fmt.Println("Handle MSG C")
MLock.Unlock()
}
func NewTransaction() {
// ...
}
上面的程式(不充分的)實作了一個 msg server,它包含了 msg dispatcher 以及不同類型訊息的 handler:
MsgDispatcher()
收到請求後會將訊息交給 goroutine 去處理。NewTransaction()
)以上面的程式來看,如果有 1000 個客戶在一秒內對這個 server 發出服務請求,等於會有數百個 goroutine 在爭搶同一塊資源,這樣一來就會拖累系統效能。
套用設計模式可以讓我們提出幾個解決問題的想法:
當 goroutine 的數量太多,可能會造成幾個問題:
所以程式設計者會需要在開發並行程式時考量 goroutine 的數量,以 dispatch handler 的例子來說,我們可以在 event consumer 後方接著一個 worker pool,確保這些 pfcp 請求會被固定數量的 worker 逐一處理。
如果 pfcpServer context 只有一個 transaction table 需要維護,我們可以考慮把更新紀錄的工作交給一個 worker 處理,這麼一來直接移除 mutex lock 也不會有 race condition 的問題發生。
前面提到的兩個方法都可以解決部分的問題。有趣的是,在實作一個系統或是套件時,我們同樣可以結合多個設計模式處理我們的需求,像是前兩個例子彼此不衝突,我們就可以提出一個更完整個 solution:
實際上在實作 pfcpServer 時可能會遇到更多問題,這時候我們就可以參考更多設計模式解決特定問題囉!
當套件的函式或成員數量多於 50 個,程式碼的可讀性與可維護性將會下降,所以多數人會在這個時間將一個套件拆成多個子套件,舉例來說:
我們可以將 pfcp 拆成:
當套件拆成多個子套件,我們必須小心的管理他們之間的依賴性,要盡量避免複雜的依賴關係圖產生。
參考以下程式碼:
package main
import "fmt"
type Server interface {
Handler
Sender
}
type Handler interface {
HandleMsg() error
}
type Sender interface {
SendMsg() error
}
type pfcpServer struct {
}
var _ Server = (*pfcpServer)(nil)
func NewPfcpServer() *pfcpServer {
return &pfcpServer{}
}
func (s *pfcpServer) HandleMsg() error {
fmt.Println("Handling msg...")
return nil
}
func (s *pfcpServer) SendMsg() error {
fmt.Println("Sending msg...")
return nil
}
func main() {
server := NewPfcpServer()
server.SendMsg()
server.HandleMsg()
}
將各個子套件的功能設計為 API interface,再由 Server
interface 統一管理這些 API。
對於任何子套件來說,他們都可以藉由 pfcpServer 物件存取到其他子套件的 API,這樣的做法有以下好處:
這篇文章除了是分享,更多的是紀錄自己學習設計模式背後精神的筆記。我想,不管是哪個設計模式或是哪種程式碼的臭味名詞都不需要死記硬背,重要的是在開發的過程中一邊發想哪個地方的設計可能會為日後的維護埋下地雷,或是哪些設計會造成程式出現很多難以發現的錯誤,學習設計模式與定期的重構就可以避免我們累積技術債造成的雪崩式效應。