拯救你的Go程式碼

DeenJun發表於2019-03-03

稍微有點標題黨,不過很短,可以看

背景

最近一直在參加開發公司新運營系統的引擎部分,寫了很多很多程式碼,也逐漸產生了一些自己對此類業務的解決方案的簡單想法。運營系統乾的事,其實就是支援運營人員配置活動。所謂活動其實可以簡單定義為:一系列條件都滿足就執行某個(些)動作。對應到程式設計師的黑話就是:一坨if else之後執行某(幾)個函式

if else

說到寫if else,大多數程式設計師可能都會會心一笑,畢竟不論寫啥系統,程式碼裡大部分語句還是if else。業務系統就是對PM的需求堆if else,基礎服務就是對OS資源和網路問題堆if else。越來越多的需求造成了程式碼的膨脹,如何來管理這坨if else,就衍生出了設計模式:通過各種手段來把if else劃分到更小的粒度。當然,還沒有任何一套方法論可以解決if else複雜性的問題,因為if else反映的其實是真實業務的需求,它其實代表了業務的複雜性。就像魯迅或者馬雲或者巴菲特說的:“不論用什麼方法,if else不會憑空消失,只會從一個地方轉移到另一個地方”。但是,在面對特定的場景,特定的業務,程式碼的閱讀性、可維護性以及擴充套件性是可以得到提升的。

規則引擎

規則引擎不是一個新東西,而且已經存在了很長時間了,市面上也有各種規則引擎。比如大家常用的iptables,就是一種規則引擎,crontab也是一種規則引擎。我們平時寫的業務邏輯,其實也是規則引擎,只是用一種不那麼明顯的方式來體現。比如:“如果重置的密碼沒有包含大小寫和標點符號,就不讓提交”,“如果使用者是抽獎送的VIP雖然不播廣告但給他彈窗”……這些業務邏輯,無論怎麼優化,要處理的程式碼分支是一定存在的。規則引擎通過自定義的一套語法(DSL),提供了程式語言所不具備的語法糖,來最大限度減少開發量。同時大部分規則都需要支援使用者隨意配置,因此DSL大多是解釋執行。使用者在後臺配置一條規則比如,遊戲中使用者線上1000分鐘後扣20的點卡:

        When
        {
           ?customer: Customer(totalTime >=1000);
        }
        Then
        {
           execute {?customer.setAmount(getAmount()-20.00);
        } 
複製程式碼

市面上比較常用的幾款規則引擎都屬於非常重量級的,使用者必須去學習它的DSL才能使用,當然基本上都是JAVA的包。這有一篇文章是幾款規則引擎的比較。沒有深度用過,我也沒法對此進行評價,但由於其概念實在太多且使用場景受限,我覺得它始終是個臨時方案,畢竟“大道至簡”。而且DSL會越寫越複雜,真正用到語法糖的地方還是少數,到最後變成另一種蹩腳的而且領域受限的程式語言了。雖然如此,但由於其規則可動態增加,老規則便於複用,對於提供通用解決方案的一些軟體公司來說,規則引擎還是很有吸引力的。畢竟程式碼不需要動,加一些規則就能再次銷售。

但是對於進行業務開發的RD來說,其實需求很簡單,如果一個東西:

  • 正確
  • 簡單
  • 能減少繁瑣的工作

那麼這便是一個有吸引力的解決方案,如果它能再快點兒,那就是一個絕妙的主意了。對於減少if else,我覺得Linq是一個非常好的方案。

Linq

Linq是C#中的一種常見技術,用過的都覺得爽。它就是一種語法糖,編譯器會編譯成真正的程式碼而不是在執行期解釋執行,因此效率也很高。Linq程式碼大概就是這樣:

        // Specify the data source.
        int[] scores = new int[] { 97, 92, 81, 60 };

        // Define the query expression.
        IEnumerable<int> scoreQuery =
            from score in scores
            where score > 80
            select score;
複製程式碼

可以看到,在程式碼中一些邏輯如果能這樣表達,那程式碼將會變得非常簡單。linq基本上就是把sql語句搬到了C#中,唯一的區別就是它把select放在了最後而sql是在前面。這其實也是因為IDE智慧提示的需求,而不是查詢結構本身要這麼設計。把select放在最後,IDE就知道你前面用過哪些變數,就可以分析出你可以select什麼從而進行提示。但是Linq是C#中的技術,和編譯器是強相關的,想移植到別的語言,還是很不容易的。當然,社群也是有一些嘗試的,比如go的移植版linq-go。然而,你懂的,由於go本身不支援泛型,編譯器也不支援linq語法,因此使用起來當然還是比較蹩腳,而且肯定是interface{}和型別推斷滿天飛了……

sql

其實我們常用的sql中的where部分是完備的,Linq也是把sql搬到了C#中。換句話說,sql的where部分其實可以組合任意的布林邏輯。所有RD都會使sql,而且是經常使sql,既然想從編譯器的方向搞事情太麻煩(尤其是go是一門以簡單著稱的語言,官方對於加關鍵字是深惡痛絕的,更別說語法糖了),那麼把mysql查詢功能幹掉,解釋執行where比較的那部分功能做成一個函式提供出來呢,比如查詢一個人是不是頂級程式設計師可以這麼查:

sql := `sex=`male` 
        and (
            dislike in (`girl`, `woman`, `female`) 
            or 
            hobby in (`dress up`, `makeup`)
        )`
複製程式碼

但是由於沒有表資料,那麼真正用於比較的資料就需要業務方自己來提供了,以上功能可以寫為:

func isTopProgrammer(userInfo User) bool {
    sql := `sex=`male` 
        and (
            dislike in (`girl`, `woman`, `female`) 
            or 
            hobby in (`dress up`, `makeup`)
        )`
    ok,_ := yql.Match(sql, map[string]interface{}{
        "sex": userInfo.Sex,
        "dislike": userInfo.Dislike,
        "hobby": userInfo.Hobbies,
    })
    return ok
}
複製程式碼

yql其實就是我最近搞的一個lib求star,求pr,求指導),幫你執行sql的比較。

如果這麼搞,其實業務邏輯中經常變動的部分可以抽象到配置檔案裡,比如:

// 先定義好不同case的處理函式
type handleFunc func(map[string]interface{}) error
var handlers = map[int]handleFunc {
    1: sendEmail,
    2: sendCoupon,
    3: sendSms,
    4: sendAlert2Boss,
    5: runAway,
}
// 從當前請求中解析資料
data := resolveDataFromPostParams(request.Body)
// 從配置檔案載入規則
rules := loadRuleFromConf()

//列舉每條規則進行匹配,如果匹配成功則執行對應handler
for _,rule := range rules {
    success,err := yql.Match(rule.SQL, data)
    if nil != err || !success {
        continue
    }
    handler := handlers[rule.ID]
    handler(data)
    break
}
複製程式碼

當然這種方式也有利有弊,比如說,把規則分離到配置檔案中,在debug時得幾個檔案之間跳來跳去地看程式碼,比較蛋疼(少寫點bug就行了,哈哈)。優點之一比如說可以利用推送平臺實時更新配置檔案,從而達到程式碼熱更新的目的。

最後

其實yql還有很多應用場景(我感覺),當然也有一些不足,比如功能還比較單一,解釋執行,快不快其實也沒和誰對比過……不過希望大家能夠嘗試嘗試,如果能夠提升工作效率,還是不錯的。求關注,求star

相關文章