如何寫出擴充套件性好的程式碼?這是我工作最近半年來一直在考慮的問題。不管自己做一套系統還是接手別人的專案,只要你的專案需要和別人互動,這個問題都是需要考慮的。我們今天只說說如何寫出擴充套件性好的函式程式碼。程式碼都以golang示例。
函式宣告
函式宣告首先是函式名字要具有自解釋性,這個要說到程式碼註釋了,這裡就不贅述了。除了函式宣告外,還有函式的形參定義。這裡以一個例子來說一下擴充套件性好的函式的引數應該如何定義。
1. 普通函式
假設我們需要一個簡單的server,我們可以像下面這樣定義,addr表示server啟動在哪個埠上。
1 |
func NewServer(addr string) |
第一期的需求很簡單,就上面這些足夠滿足了。專案上線跑了一段時間發現,由於連線沒有設定超時,很多連線一直得不到釋放(異常情況),嚴重影響伺服器效能。好,那第二期我們加個timeout。
1 |
func NewServer(addr string, timeout time.Duration) |
這個時候尷尬的情況出現了,呼叫你程式碼的所有人都需要改動程式碼。而且這只是一個改動,之後如果要支援tls,那麼又得改動一次。
2. 不定引數
解決上面的窘境的一種方法是使用不定引數。下面先簡單介紹一下不定引數。第一次接觸不定引數是學習C語言中的Hello World
的程式碼中printf
,宣告如下
1 |
static int printf(const char *fmt, ...) |
C的函式呼叫可以簡單看成call/ret
,call
的時候會把當前的IP儲存起來,然後將函式地址以及函式引數入棧。printf的fmt中儲存了引數的型別(%d表示int,%s表示string)並能計算出個數,這樣就能找到每個具體的引數是什麼了。golang也是支援不定引數的,比如我要實現一個整數加法。
1 2 3 4 5 6 7 8 9 10 11 |
func Add(list ...int) int { sum := 0 for _, x := range list { sum += x } return sum } func main() { fmt.Println(Add(1,2,3)) //6 } |
上面是所有的變參都是同一種型別,如果是不同的型別可以使用interface,使用反射來判斷其型別。
1 2 3 4 5 6 7 |
func Varargs(list ...interface{}) { for _, x := range list { if reflect.ValueOf(x).Kind() == int { // } } } |
但是如果是我們自己定義的函式的話,型別通常是知道的,也就不需要上面那麼麻煩地再去判斷一次,可以直接進行型別轉換。
1 2 3 4 5 |
func Varargs(list ...interface{}) { //通過interface.(type)將interface型別轉換成type型別 fmt.Println(list[0].(int)) fmt.Println(list[1].(string)) } |
但是這麼做比較危險,使用的時候必須嚴格按照說明進行傳參,任何一種型別不正確,程式將panic。還有一個問題就是不定引數不能為空,或者說傳入的實參必須是形參的一個嚴格字首。
3. 封裝成 struct
相比於上面兩種方法更好一點的是把所有引數封裝成struct,這樣函式宣告看起來很簡單。
1 2 3 4 5 6 7 |
type Param struct { x int y string ... } func Varargs(p *Param) {} |
封裝成struct的方式應該是一種對引數比較好的組織形式,之後函式不管怎麼擴張,只需要增加struct成員就好,而不需要改變函式宣告瞭。而struct的壞處在什麼地方呢?比如上面的Param.x是int型,如果我們不設定x,也就是下面這樣傳參。
1 2 3 4 5 |
p := &Param{ y: "hello", } Varargs(p) |
這個時候Varargs看到的Param.x的0。你讓Varargs怎麼想?使用者沒有設定x(忘記設定?想使用預設值?)?使用者把x設定成0?這真的有點尷尬。但是這個問題還是有解決方案的?1.避開預設值,int型不使用0,string型別不使用””。2.使用指標,使用者沒有設定的時候x==nil,設定的時候對x解引用(*x)取得值。這兩種方式不管怎麼來看,都是十分的反人類,一點也不simple。
4. option
option的方式的最早是由 Rob Pike 提出,Rob Pike就不做介紹了,感興趣的可以看他的wiki連線。我們把option引數封裝成一個函式傳給我們的目標函式,所有相關的工作由函式來做。舉個例子,我們現在要寫個Server,timeout和tls都是可選項,那麼可以像下面這麼來寫(所有error handle都省去)。
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 |
func NewServer(addr string, options ...func(*Server)) (*Server, error) { srv := &Server{ Addr: addr, } for _, option := range options { option(srv) } return srv } func timeout(d time.Duration) func(*Server) { return func(srv *Server) { srv.timeout = d } } func tls(c *config) func(*Server) { return func(srv *Server) { Tls := loadConfig(c) srv.tls = Tls } } //使用 src, err = NewServer("localhost:8080", timeout(1), tls(path/to/cert)) |
這麼寫的好處一目瞭然,橫向擴充套件起來特別方便,而且解決上面的提到的基本所有的問題。
函式實現
正常單一功能的函式實現沒有什麼好說的。如果需要根據不同的條件來執行不同的行為的話,這個應該怎麼做的?舉個例子,我現在在公司做一個優惠券的專案,使用者領券和使用券的時候有一些規則,比如每人每日限領3張等。這些規則肯定不會一成不變,也許第一期是2個規則,第二期就變成4個規則了。正常可能會像下面這麼寫。
1 2 3 4 5 6 7 8 9 |
func ruleVerify() { //process if cond1 { // } else if cond2 { // } ... } |
或者用switch-case。雖然很多人說switch-case寫起來要比if-else更好看或者高階一點,其實我並不這麼覺得。if-else和switch-case本質上並沒有什麼區別,擴充套件的時候如果需要多加一個條件分支,這兩種方法改動起來都比較醜。下面說說我的解決方案。
1. 類工廠模式
熟悉設計模式的肯定對工廠模式肯定不會陌生。工廠模式的意思是通過引數來決定生成什麼樣的物件例項。我這裡並不是說直接使用工廠模式而是使用工廠模式這種思想來程式設計。舉個典型的例子,webserver的router實現方式:根據不同的路由(/foo,/bar)對應到不同的handler。光這麼說,可能很多人還是不明白這種方式的擴充套件性好在什麼地方。下面從0到1來感受一下。
首先根據不同的條件對應不同的handler,這個最簡單的是使用Map來實現,沒有問題,但是map裡面存什麼呢?如果我要增加一個條件以及對應的處理函式的時候怎麼做呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//存放 <cond, handler> 對應關係 var mux map[string]func(option *Option) error //註冊handler func register(key string, f func(option *Option) error) { if mux == nil { mux = make(map[string]func(option *Option) error) } if _, exist := mux[key]; exist { return errors.New("handler exist") } mux[key] = f } //factory func factory(option *Option) error { return mux[option.Key](option) } |
程式碼主要分三個部分:1.mux用來存放cond和handler的對應關係;2.register用來註冊新的handler; 3.提供給外部的程式碼入口。下面到了最核心的問題了,如果某一天PM和你說:大神,我們現在要新加一個使用者用券規則。這個時候你就可以和她說:沒問題。程式碼上的改動只需要實現一個新增規則的實現函式,同時呼叫一下register即可。
總結
踏出校門一年多了,我經常在想什麼樣的程式碼才是好的程式碼?我相信每個人都會有不同的答案。從我個人角度來看,擴充套件性確實是衡量好程式碼的一個很重要的指標。在做業務系統的時候經常為了趕進度寫的程式碼而忽略擴充套件性,最後隨著版本迭代發現之前的程式碼框架越來越臃腫,不得不進行重構。而且重構,從某種意義上來說,就是填坑。
參考
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式